Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update operations do not update the td id correctly #18

Merged
merged 8 commits into from
Sep 6, 2022
15 changes: 12 additions & 3 deletions src/api-reference/things/things.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Injectable } from '@nestjs/common';
import { generate as generatePatch, apply as mergePatch } from 'json-merge-patch';

import {
DuplicateIdException,
InvalidThingDescriptionException,
MismatchIdException,
NonAnonymousThingDescription,
Expand Down Expand Up @@ -64,15 +65,16 @@ export class ThingsService {
const now = new Date().toISOString();
const internalThingDescription = await this.thingDescriptionRepository.findFirst({ where: { urn: id } });
if (internalThingDescription) {
await this.assertUniqueThingDescriptionId(internalThingDescription.urn, dto.id);
await this.thingDescriptionRepository.update({
where: { id: internalThingDescription.id },
data: { json: dto, modified: now },
data: { json: dto, urn: dto.id, modified: now },
});
const patch = generatePatch(internalThingDescription.json, dto) ?? {};
this.eventsService.emitUpdated({ id, ...patch });
return true;
} else {
if ( id !== dto.id) throw new MismatchIdException();
if (id !== dto.id) throw new MismatchIdException();
await this.thingDescriptionRepository.create({
urn: id,
json: dto,
Expand All @@ -93,11 +95,12 @@ export class ThingsService {

const patchedThingDescription = mergePatch(internalThingDescription.json, dto);
this.requireValidThingDescription(patchedThingDescription);
await this.assertUniqueThingDescriptionId(internalThingDescription.urn, patchedThingDescription.id);
const now = new Date().toISOString();

await this.thingDescriptionRepository.update({
where: { id: internalThingDescription.id },
data: { json: patchedThingDescription, modified: now },
data: { json: patchedThingDescription, urn: patchedThingDescription.id, modified: now },
});

this.eventsService.emitUpdated({ id: internalThingDescription.urn, ...dto });
Expand Down Expand Up @@ -127,4 +130,10 @@ export class ThingsService {
throw new InvalidThingDescriptionException(errors);
}
}

private async assertUniqueThingDescriptionId(currentId: string, newId: string | undefined): Promise<void> {
if (!newId || newId === currentId) return;
const idExist = await this.thingDescriptionRepository.exist({ where: { urn: newId } });
if (idExist) throw new DuplicateIdException(newId as string);
}
}
12 changes: 12 additions & 0 deletions src/common/exceptions/duplicate-id.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ProblemDetailsException } from './problem-details.exception';

export class DuplicateIdException extends ProblemDetailsException {
public constructor(id: string) {
super({
type: '/errors/types/duplicate-id',
title: 'Duplicate Id',
status: 409,
detail: `The id ${id} is already in use by another Thing Description`,
});
}
}
1 change: 1 addition & 0 deletions src/common/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export * from './invalid-credentials.exception';
export * from './invalid-thing-description.exception';
export * from './invalid-token.exception';
export * from './non-anonymous-thing-description.exception';
export * from './duplicate-id.exception';
export * from './mismatch-id.exception';
89 changes: 88 additions & 1 deletion test/e2e/things.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,31 @@ describe('/things', () => {
});
});

it('should fail to update the Thing Description id if the id is already exist', async () => {
const thingDescriptions = [];
const id = `${validThingDescription.id}:${getShortUnique()}`;
for (let i = 0; i < 2; i++) {
const validThingDescriptionObject = validThingDescription;
validThingDescriptionObject.id = `${id}:${i}`;
thingDescriptions.push(validThingDescriptionObject);
const response = await axios.put(`/things/${validThingDescriptionObject.id}`, validThingDescriptionObject, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
});
expect(response.status).toBe(201);
}
const { status, data } = await axios.put(`/things/${id}:0`, thingDescriptions[1], {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
});

expect(status).toBe(409);
expect(data).toMatchObject({
type: '/errors/types/duplicate-id',
title: 'Duplicate Id',
status: 409,
detail: `The id ${validThingDescription.id} is already in use by another Thing Description`,
});
});

it('should update the Thing Description', async () => {
const { headers } = await axios.post('/things', validAnonymousThingDescription, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
Expand All @@ -270,11 +295,28 @@ describe('/things', () => {
});
});

it('should update the Thing Description id and update the URL urn', async () => {
const { headers } = await axios.post('/things', validAnonymousThingDescription, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
});
const id = `urn:dev:ops:${getShortUnique()}`;
const updatedThingDescription = { ...validAnonymousThingDescription, id: id };

await axios.put(headers.location, updatedThingDescription, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
});

const { status } = await axios.get(headers.location);
expect(status).toBe(404);

const { data } = await axios.get(`/things/${id}`);
expect(data).toMatchObject(updatedThingDescription);
});

it('should create the Thing Description', async () => {
const id = `${validThingDescription.id}:${getShortUnique()}`;
const validThingDescriptionObject = validThingDescription;
validThingDescriptionObject.id = id;
console.log(validThingDescriptionObject.id);
const { status } = await axios.put(`/things/${id}`, validThingDescriptionObject, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
});
Expand Down Expand Up @@ -323,6 +365,31 @@ describe('/things', () => {
});
});

it('should fail to update the Thing Description id if the id is already exist', async () => {
const thingDescriptions = [];
const id = `${validThingDescription.id}:${getShortUnique()}`;
for (let i = 0; i < 2; i++) {
const validThingDescriptionObject = validThingDescription;
validThingDescriptionObject.id = `${id}:${i}`;
thingDescriptions.push(validThingDescriptionObject);
const response = await axios.put(`/things/${validThingDescriptionObject.id}`, validThingDescriptionObject, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
});
expect(response.status).toBe(201);
}
const { status, data } = await axios.patch(`/things/${id}:0`, thingDescriptions[1], {
headers: { Authorization: `Bearer ${defaultAccessToken}`, 'Content-Type': 'application/merge-patch+json' },
});

expect(status).toBe(409);
expect(data).toMatchObject({
type: '/errors/types/duplicate-id',
title: 'Duplicate Id',
status: 409,
detail: `The id ${validThingDescription.id} is already in use by another Thing Description`,
});
});

it('should fail to update the Thing Description when the patched Thing Description is invalid', async () => {
const { headers } = await axios.post('/things', validAnonymousThingDescription, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
Expand Down Expand Up @@ -366,6 +433,26 @@ describe('/things', () => {
...modifiedParts,
});
});

it('should update the Thing Description id and update the URL urn', async () => {
const { headers } = await axios.post('/things', validAnonymousThingDescription, {
headers: { Authorization: `Bearer ${defaultAccessToken}` },
});
const modifiedId = { id: `urn:dev:ops:${getShortUnique()}` };

await axios.patch(headers.location, modifiedId, {
headers: { Authorization: `Bearer ${defaultAccessToken}`, 'Content-Type': 'application/merge-patch+json' },
});

const { status } = await axios.get(headers.location);
expect(status).toBe(404);

const { data } = await axios.get(`/things/${modifiedId.id}`);
expect(data).toStrictEqual({
...validAnonymousThingDescription,
...modifiedId,
});
});
});

describe('DELETE', () => {
Expand Down