Skip to content

Commit

Permalink
feat(directory-service): adding resource API endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
tzuge committed Jan 23, 2025
1 parent e8b7e14 commit 2089efd
Show file tree
Hide file tree
Showing 14 changed files with 895 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('configuration', () => {
getTaggedResources: jest.fn(),
applyTag: jest.fn(),
removeTag: jest.fn(),
getResources: jest.fn(),
saveResource: jest.fn(),
deleteResource: jest.fn(),
};
Expand Down
38 changes: 19 additions & 19 deletions apps/directory-service/src/directory/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { DomainEvent, DomainEventDefinition, User, Stream } from '@abgov/adsp-service-sdk';
import type { mapTag } from './mapper';
import { Resource } from './types';
import type { DomainEvent, DomainEventDefinition, User, Stream, AdspId } from '@abgov/adsp-service-sdk';
import type { mapResource, mapTag } from './mapper';

const ENTRY_UPDATED = 'entry-updated';
const ENTRY_DELETED = 'entry-deleted';
Expand All @@ -9,6 +8,7 @@ const UNTAGGED_RESOURCE = 'untagged-resource';
const RESOURCE_RESOLUTION_FAILED = 'resource-resolution-failed';

type Tag = ReturnType<typeof mapTag>;
type Resource = ReturnType<typeof mapResource>;

export const EntryUpdatedDefinition: DomainEventDefinition = {
name: ENTRY_UPDATED,
Expand Down Expand Up @@ -207,20 +207,24 @@ export const entryDeleted = (
},
});

export const taggedResource = (resource: Resource, tag: Tag, updatedBy: User, isNewResource: boolean): DomainEvent => ({
export const taggedResource = (
tenantId: AdspId,
resource: Resource,
tag: Tag,
updatedBy: User,
isNewResource: boolean
): DomainEvent => ({
name: TAGGED_RESOURCE,
timestamp: new Date(),
tenantId: resource.tenantId,
tenantId,
correlationId: tag.value,
context: {
tag: tag.value,
resources: resource.urn.toString(),
},
payload: {
resource: {
urn: resource.urn.toString(),
name: resource.name,
description: resource.description,
...resource,
isNew: isNewResource,
},
tag,
Expand All @@ -231,21 +235,17 @@ export const taggedResource = (resource: Resource, tag: Tag, updatedBy: User, is
},
});

export const untaggedResource = (resource: Resource, tag: Tag, updatedBy: User): DomainEvent => ({
export const untaggedResource = (tenantId: AdspId, resource: Resource, tag: Tag, updatedBy: User): DomainEvent => ({
name: UNTAGGED_RESOURCE,
timestamp: new Date(),
tenantId: resource.tenantId,
tenantId,
correlationId: tag.value,
context: {
tag: tag.value,
resources: resource.urn.toString(),
},
payload: {
resource: {
urn: resource.urn.toString(),
name: resource.name,
description: resource.description,
},
resource,
tag,
updatedBy: {
id: updatedBy.id,
Expand All @@ -254,18 +254,18 @@ export const untaggedResource = (resource: Resource, tag: Tag, updatedBy: User):
},
});

export const resourceResolutionFailed = (resource: Resource, type: string, error: string): DomainEvent => ({
export const resourceResolutionFailed = (tenantId: AdspId, urn: AdspId, type: string, error: string): DomainEvent => ({
name: RESOURCE_RESOLUTION_FAILED,
timestamp: new Date(),
tenantId: resource.tenantId,
tenantId,
correlationId: type,
context: {
resources: resource.urn.toString(),
resources: urn.toString(),
type,
},
payload: {
resource: {
urn: resource.urn.toString(),
urn: urn.toString(),
},
type,
error,
Expand Down
2 changes: 1 addition & 1 deletion apps/directory-service/src/directory/job/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function createResolveJob({ logger, tokenProvider, configurationService,
context: 'ResolveJob',
tenant: tenantId.toString(),
});
eventService.send(resourceResolutionFailed(resource, type?.type, `${err}`));
eventService.send(resourceResolutionFailed(tenantId, urn, type?.type, `${err}`));
}
}
};
Expand Down
15 changes: 13 additions & 2 deletions apps/directory-service/src/directory/mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export function mapTag(apiId: AdspId, tag: Tag) {
label: tag.label,
value: tag.value,
_links: {
self: {
href: `${apiId}:/tags/${tag.value}`,
},
resources: {
href: `${apiId}:/tags/${tag.value}/resources`,
},
Expand All @@ -16,15 +19,23 @@ export function mapTag(apiId: AdspId, tag: Tag) {
: null;
}

export function mapResource(resource: Resource) {
export function mapResource(apiId: AdspId, resource: Resource) {
return resource
? {
urn: resource.urn.toString(),
name: resource.name,
description: resource.description,
type: resource.type,
_links: {
represents: { href: resource.urn.toString() },
self: {
href: `${apiId}:/resources/${encodeURIComponent(resource.urn.toString())}`,
},
represents: {
href: resource.urn.toString(),
},
tags: {
href: `${apiId}:/resources/${resource.urn}/tags`,
},
},
_embedded: resource.data && {
represents: resource.data,
Expand Down
35 changes: 31 additions & 4 deletions apps/directory-service/src/directory/model/resource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('ResourceType', () => {
getTaggedResources: jest.fn(),
applyTag: jest.fn(),
removeTag: jest.fn(),
getResources: jest.fn(),
saveResource: jest.fn(),
deleteResource: jest.fn(),
};
Expand All @@ -41,6 +42,7 @@ describe('ResourceType', () => {
axiosMock.isAxiosError.mockClear();
directoryMock.getResourceUrl.mockClear();
repositoryMock.deleteResource.mockClear();
repositoryMock.saveResource.mockClear();
});

it('can be created', () => {
Expand Down Expand Up @@ -82,7 +84,7 @@ describe('ResourceType', () => {
});

describe('resolve', () => {
it('can resolve resource', async () => {
it('can resolve resource and sync', async () => {
const type = new ResourceType(loggerMock, directoryMock, repositoryMock, {
type: 'test',
matcher: '^\\/tests',
Expand All @@ -99,7 +101,7 @@ describe('ResourceType', () => {
axiosMock.get.mockResolvedValueOnce({ data: { testName: name, testDescription: description } });
repositoryMock.saveResource.mockImplementationOnce((resource) => Promise.resolve(resource));

const result = await type.resolve('token', { tenantId, urn });
const result = await type.resolve('token', { tenantId, urn }, true);
expect(result.tenantId).toBe(tenantId);
expect(result.urn).toBe(urn);
expect(result.name).toBe(name);
Expand All @@ -109,6 +111,31 @@ describe('ResourceType', () => {
);
});

it('can resolve resource and not sync', async () => {
const type = new ResourceType(loggerMock, directoryMock, repositoryMock, {
type: 'test',
matcher: '^\\/tests',
namePath: 'testName',
descriptionPath: 'testDescription',
});

const resourceUrl = new URL('https://test-service.com/test/v1/tests/123');
directoryMock.getResourceUrl.mockResolvedValueOnce(resourceUrl);

const urn = adspId`urn:ads:platform:test-service:v1:/tests/123`;
const name = 'Test 123';
const description = 'This is a description.';
axiosMock.get.mockResolvedValueOnce({ data: { testName: name, testDescription: description } });
repositoryMock.saveResource.mockImplementationOnce((resource) => Promise.resolve(resource));

const result = await type.resolve('token', { tenantId, urn });
expect(result.tenantId).toBe(tenantId);
expect(result.urn).toBe(urn);
expect(result.name).toBe(name);
expect(result.description).toBe(description);
expect(repositoryMock.saveResource).not.toHaveBeenCalled();
});

it('can default description getter', async () => {
const type = new ResourceType(loggerMock, directoryMock, repositoryMock, {
type: 'test',
Expand Down Expand Up @@ -189,7 +216,7 @@ describe('ResourceType', () => {
await expect(type.resolve('token', null)).rejects.toThrow(Error);
});

it('can delete not found resource', async () => {
it('can delete not found resource if sync true', async () => {
const type = new ResourceType(loggerMock, directoryMock, repositoryMock, {
type: 'test',
matcher: '^\\/tests',
Expand All @@ -214,7 +241,7 @@ describe('ResourceType', () => {
expect(repositoryMock.deleteResource).toHaveBeenCalledWith(expect.objectContaining({ tenantId, urn }));
});

it('can not delete on not found resource', async () => {
it('can not delete on not found resource if sync false', async () => {
const type = new ResourceType(loggerMock, directoryMock, repositoryMock, {
type: 'test',
matcher: '^\\/tests',
Expand Down
8 changes: 5 additions & 3 deletions apps/directory-service/src/directory/model/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class ResourceType {
return this.matcher.test(urn.resource);
}

public async resolve(token: string, resource: Resource, deleteNotFound = false): Promise<Resource> {
public async resolve(token: string, resource: Resource, sync = false): Promise<Resource> {
if (!this.matches(resource?.urn)) {
throw new InvalidOperationError(`Resource type '${this.type}' not matched to resource: ${resource.urn}`);
}
Expand Down Expand Up @@ -64,12 +64,14 @@ export class ResourceType {
const name = this.nameGetter(data) || resource.name;
const description = this.descriptionGetter(data) || resource.description;
if (name !== resource.name || description !== resource.description) {
resource = await this.repository.saveResource({ ...resource, name, description, type: this.type });
resource = sync
? await this.repository.saveResource({ ...resource, name, description, type: this.type })
: { ...resource, name, description };
}

return { ...resource, data };
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 404 && deleteNotFound) {
if (axios.isAxiosError(err) && err.response?.status === 404 && sync) {
// If there's a 404, then the resource doesn't exist. This can happen if the resource was deleted before being resolved.
// Delete the missing resource for consistency.
await this.repository.deleteResource(resource);
Expand Down
12 changes: 10 additions & 2 deletions apps/directory-service/src/directory/repository/directory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AdspId } from '@abgov/adsp-service-sdk';
import { Results } from '@core-services/core-common';
import { DirectoryEntity } from '../model';
import { Directory, Criteria, Tag, Resource, TagCriteria } from '../types';
import { Directory, Criteria, Tag, Resource, TagCriteria, ResourceCriteria } from '../types';

export interface DirectoryRepository {
find(top: number, after: string, criteria: Criteria): Promise<Results<DirectoryEntity>>;
Expand All @@ -11,13 +11,21 @@ export interface DirectoryRepository {
save(type: DirectoryEntity): Promise<DirectoryEntity>;

getTags(top: number, after: string, criteria: TagCriteria): Promise<Results<Tag>>;
getTaggedResources(tenantId: AdspId, tag: string, top: number, after: string): Promise<Results<Resource>>;
getTaggedResources(
tenantId: AdspId,
tag: string,
top: number,
after: string,
criteria: ResourceCriteria
): Promise<Results<Resource>>;

applyTag(
tag: Tag,
resource: Resource
): Promise<{ tag: Tag; resource: Resource; tagged: boolean; isNewResource: boolean }>;
removeTag(tag: Tag, resource: Resource): Promise<{ tag?: Tag; resource?: Resource; untagged: boolean }>;

getResources(top: number, after: string, criteria: ResourceCriteria);
saveResource(resource: Resource & { type?: string }): Promise<Resource>;
deleteResource(resource: Resource): Promise<boolean>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe('router', () => {
getTaggedResources: jest.fn(),
applyTag: jest.fn(),
removeTag: jest.fn(),
getResources: jest.fn(),
saveResource: jest.fn(),
deleteResource: jest.fn(),
};
Expand Down
Loading

0 comments on commit 2089efd

Please sign in to comment.