diff --git a/apps/directory-service/src/directory/events.ts b/apps/directory-service/src/directory/events.ts index 2d77d0d15..fc10a11a9 100644 --- a/apps/directory-service/src/directory/events.ts +++ b/apps/directory-service/src/directory/events.ts @@ -1,5 +1,6 @@ import type { DomainEvent, DomainEventDefinition, User, Stream } from '@abgov/adsp-service-sdk'; -import { Resource, Tag } from './types'; +import type { mapTag } from './mapper'; +import { Resource } from './types'; const ENTRY_UPDATED = 'entry-updated'; const ENTRY_DELETED = 'entry-deleted'; @@ -7,6 +8,8 @@ export const TAGGED_RESOURCE = 'tagged-resource'; const UNTAGGED_RESOURCE = 'untagged-resource'; const RESOURCE_RESOLUTION_FAILED = 'resource-resolution-failed'; +type Tag = ReturnType; + export const EntryUpdatedDefinition: DomainEventDefinition = { name: ENTRY_UPDATED, description: 'Signalled when a directory entry is updated.', @@ -85,6 +88,7 @@ export const TaggedResourceDefinition: DomainEventDefinition = { tag: { type: 'object', properties: { + urn: { type: 'string' }, label: { type: 'string' }, value: { type: 'string' }, }, @@ -117,6 +121,7 @@ export const UntaggedResourceDefinition: DomainEventDefinition = { tag: { type: 'object', properties: { + urn: { type: 'string' }, label: { type: 'string' }, value: { type: 'string' }, }, @@ -218,10 +223,7 @@ export const taggedResource = (resource: Resource, tag: Tag, updatedBy: User, is description: resource.description, isNew: isNewResource, }, - tag: { - label: tag.label, - value: tag.value, - }, + tag, updatedBy: { id: updatedBy.id, name: updatedBy.name, @@ -244,10 +246,7 @@ export const untaggedResource = (resource: Resource, tag: Tag, updatedBy: User): name: resource.name, description: resource.description, }, - tag: { - label: tag.label, - value: tag.value, - }, + tag, updatedBy: { id: updatedBy.id, name: updatedBy.name, diff --git a/apps/directory-service/src/directory/index.ts b/apps/directory-service/src/directory/index.ts index 4c99f900a..822cf840c 100644 --- a/apps/directory-service/src/directory/index.ts +++ b/apps/directory-service/src/directory/index.ts @@ -11,6 +11,7 @@ import { AdspId, ServiceDirectory, TokenProvider, + adspId, } from '@abgov/adsp-service-sdk'; import { assertAuthenticatedHandler, DomainEvent, WorkQueueService } from '@core-services/core-common'; import { createDirectoryJobs } from './job'; @@ -59,6 +60,7 @@ export const applyDirectoryMiddleware = ( const resourceRouter = createResourceRouter({ logger, + apiId: adspId`${serviceId}:resource-v1`, directory, eventService, repository: directoryRepository, diff --git a/apps/directory-service/src/directory/mapper.ts b/apps/directory-service/src/directory/mapper.ts new file mode 100644 index 000000000..e22071211 --- /dev/null +++ b/apps/directory-service/src/directory/mapper.ts @@ -0,0 +1,34 @@ +import { AdspId } from '@abgov/adsp-service-sdk'; +import { Tag, Resource } from './types'; + +export function mapTag(apiId: AdspId, tag: Tag) { + return tag + ? { + urn: `${apiId}:/tags/${tag.value}`, + label: tag.label, + value: tag.value, + _links: { + resources: { + href: `${apiId}:/tags/${tag.value}/resources`, + }, + }, + } + : null; +} + +export function mapResource(resource: Resource) { + return resource + ? { + urn: resource.urn.toString(), + name: resource.name, + description: resource.description, + type: resource.type, + _links: { + represents: { href: resource.urn.toString() }, + }, + _embedded: resource.data && { + represents: resource.data, + }, + } + : null; +} diff --git a/apps/directory-service/src/directory/router/resource.spec.ts b/apps/directory-service/src/directory/router/resource.spec.ts index 81fdc02b2..a02106889 100644 --- a/apps/directory-service/src/directory/router/resource.spec.ts +++ b/apps/directory-service/src/directory/router/resource.spec.ts @@ -7,6 +7,7 @@ import { createResourceRouter, getTaggedResources, getTags, tagOperation } from describe('resource', () => { const tenantId = adspId`urn:ads:platform:tenant-service:v2:/tenants/test`; + const apiId = adspId`urn:ads:platform:directory-service:resource-v1`; const loggerMock = { debug: jest.fn(), @@ -49,6 +50,7 @@ describe('resource', () => { it('can create router', () => { const router = createResourceRouter({ + apiId, logger: loggerMock, directory: directoryMock, eventService: eventServiceMock, @@ -59,7 +61,7 @@ describe('resource', () => { describe('getTags', () => { it('can create handler', () => { - const handler = getTags(repositoryMock); + const handler = getTags(apiId, repositoryMock); expect(handler).toBeTruthy(); }); @@ -81,7 +83,7 @@ describe('resource', () => { ]; repositoryMock.getTags.mockResolvedValueOnce({ results, page }); - const handler = getTags(repositoryMock); + const handler = getTags(apiId, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(repositoryMock.getTags).toHaveBeenCalledWith( 10, @@ -114,7 +116,7 @@ describe('resource', () => { ]; repositoryMock.getTags.mockResolvedValueOnce({ results, page }); - const handler = getTags(repositoryMock); + const handler = getTags(apiId, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(repositoryMock.getTags).toHaveBeenCalledWith( 42, @@ -138,7 +140,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = getTags(repositoryMock); + const handler = getTags(apiId, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); expect(next).toBeCalledWith(expect.any(UnauthorizedUserError)); @@ -147,7 +149,7 @@ describe('resource', () => { describe('tagOperation', () => { it('can create handler', () => { - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); expect(handler).toBeTruthy(); }); @@ -179,7 +181,7 @@ describe('resource', () => { }; repositoryMock.applyTag.mockResolvedValueOnce({ tag, resource, tagged: true }); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(repositoryMock.applyTag).toHaveBeenCalledWith( expect.objectContaining({ tenantId, ...tag }), @@ -227,7 +229,7 @@ describe('resource', () => { }; repositoryMock.removeTag.mockResolvedValueOnce({ tag, resource, untagged: true }); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(repositoryMock.removeTag).toHaveBeenCalledWith( expect.objectContaining({ tenantId, ...tag }), @@ -275,7 +277,7 @@ describe('resource', () => { }; repositoryMock.applyTag.mockResolvedValueOnce({ tag, resource, tagged: true }); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(repositoryMock.applyTag).toHaveBeenCalledWith( expect.objectContaining({ tenantId, ...req.body.tag }), @@ -312,7 +314,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); @@ -337,7 +339,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); @@ -359,7 +361,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); @@ -382,7 +384,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); @@ -404,7 +406,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); @@ -429,7 +431,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); @@ -454,7 +456,7 @@ describe('resource', () => { const res = { send: jest.fn() }; const next = jest.fn(); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); @@ -481,7 +483,7 @@ describe('resource', () => { directoryMock.getResourceUrl.mockResolvedValueOnce(null); - const handler = tagOperation(loggerMock, directoryMock, eventServiceMock, repositoryMock); + const handler = tagOperation(apiId, loggerMock, directoryMock, eventServiceMock, repositoryMock); await handler(req as unknown as Request, res as unknown as Response, next); expect(res.send).not.toHaveBeenCalled(); diff --git a/apps/directory-service/src/directory/router/resource.swagger.yml b/apps/directory-service/src/directory/router/resource.swagger.yml index be9f2a171..fecf2e891 100644 --- a/apps/directory-service/src/directory/router/resource.swagger.yml +++ b/apps/directory-service/src/directory/router/resource.swagger.yml @@ -3,10 +3,19 @@ components: Tag: type: object properties: + urn: + type: string label: type: string value: type: string + _links: + type: object + additionalProperties: + type: object + properties: + href: + type: string Resource: type: object properties: diff --git a/apps/directory-service/src/directory/router/resource.ts b/apps/directory-service/src/directory/router/resource.ts index 3f6bf684b..e93e9ab31 100644 --- a/apps/directory-service/src/directory/router/resource.ts +++ b/apps/directory-service/src/directory/router/resource.ts @@ -13,38 +13,12 @@ import { body, query } from 'express-validator'; import { Logger } from 'winston'; import { DirectoryRepository } from '../repository'; import { ServiceRoles } from '../roles'; -import { Resource, Tag } from '../types'; -import { TAG_OPERATION_TAG, TAG_OPERATION_UNTAG, TagOperationRequests } from './types'; import { taggedResource, untaggedResource } from '../events'; import { DirectoryConfiguration } from '../configuration'; +import { mapTag, mapResource } from '../mapper'; +import { TAG_OPERATION_TAG, TAG_OPERATION_UNTAG, TagOperationRequests } from './types'; -function mapTag(tag: Tag) { - return tag - ? { - label: tag.label, - value: tag.value, - } - : null; -} - -function mapResource(resource: Resource) { - return resource - ? { - urn: resource.urn.toString(), - name: resource.name, - description: resource.description, - type: resource.type, - _links: { - represents: { href: resource.urn.toString() }, - }, - _embedded: resource.data && { - represents: resource.data, - }, - } - : null; -} - -export function getTags(repository: DirectoryRepository): RequestHandler { +export function getTags(apiId: AdspId, repository: DirectoryRepository): RequestHandler { return async (req, res, next) => { try { const user = req.user; @@ -62,7 +36,7 @@ export function getTags(repository: DirectoryRepository): RequestHandler { resourceUrnEquals: resource, }); res.send({ - results: results.map(mapTag), + results: results.map((result) => mapTag(apiId, result)), page, }); } catch (err) { @@ -72,6 +46,7 @@ export function getTags(repository: DirectoryRepository): RequestHandler { } export function tagOperation( + apiId: AdspId, logger: Logger, directory: ServiceDirectory, eventService: EventService, @@ -137,28 +112,30 @@ export function tagOperation( case TAG_OPERATION_TAG: { const { tag, resource, tagged, isNewResource } = await repository.applyTag(targetTag, targetResource); + const tagResult = mapTag(apiId, tag); result = { tagged, - tag: mapTag(tag), + tag: tagResult, resource: mapResource(resource), }; if (tagged) { - event = taggedResource(resource, tag, user, isNewResource); + event = taggedResource(resource, tagResult, user, isNewResource); } break; } case TAG_OPERATION_UNTAG: { const { tag, resource, untagged } = await repository.removeTag(targetTag, targetResource); + const tagResult = mapTag(apiId, tag); result = { untagged, - tag: mapTag(tag), + tag: tagResult, resource: mapResource(resource), }; if (untagged) { - event = untaggedResource(resource, tag, user); + event = untaggedResource(resource, tagResult, user); } break; } @@ -233,13 +210,14 @@ export function getTaggedResources(repository: DirectoryRepository): RequestHand } interface ResourceRouterProps { + apiId: AdspId; logger: Logger; directory: ServiceDirectory; eventService: EventService; repository: DirectoryRepository; } -export function createResourceRouter({ logger, directory, eventService, repository }: ResourceRouterProps) { +export function createResourceRouter({ apiId, logger, directory, eventService, repository }: ResourceRouterProps) { const router = Router(); router.get( @@ -249,7 +227,7 @@ export function createResourceRouter({ logger, directory, eventService, reposito query('after').optional().isString(), query('resource').optional().isString().isLength({ min: 1, max: 2000 }) ), - getTags(repository) + getTags(apiId, repository) ); router.post( '/tags', @@ -266,7 +244,7 @@ export function createResourceRouter({ logger, directory, eventService, reposito body('resource.name').optional().isString().isLength({ min: 1, max: 250 }), body('resource.description').optional().isString().isLength({ min: 1, max: 2000 }) ), - tagOperation(logger, directory, eventService, repository) + tagOperation(apiId, logger, directory, eventService, repository) ); router.get( diff --git a/apps/directory-service/src/main.ts b/apps/directory-service/src/main.ts index cfb9042e1..5eacaa9da 100644 --- a/apps/directory-service/src/main.ts +++ b/apps/directory-service/src/main.ts @@ -6,7 +6,13 @@ import { Strategy as AnonymousStrategy } from 'passport-anonymous'; import * as compression from 'compression'; import * as cors from 'cors'; import * as helmet from 'helmet'; -import { AdspId, initializePlatform, instrumentAxios, ServiceMetricsValueDefinition } from '@abgov/adsp-service-sdk'; +import { + adspId, + AdspId, + initializePlatform, + instrumentAxios, + ServiceMetricsValueDefinition, +} from '@abgov/adsp-service-sdk'; import type { User } from '@abgov/adsp-service-sdk'; import { createLogger, createErrorHandler, createAmqpConfigUpdateService } from '@core-services/core-common'; import { environment } from './environments/environment'; @@ -119,6 +125,30 @@ const initializeApp = async (): Promise => { directoryUrl: new URL(environment.DIRECTORY_URL), eventStreams: [EntryUpdatesStream], values: [ServiceMetricsValueDefinition], + serviceConfigurations: [ + { + serviceId: adspId`urn:ads:platform:cache-service`, + configuration: { + targets: { + [`${serviceId}:resource-v1`]: { + ttl: 8 * 60 * 60, + invalidationEvents: [ + { + namespace: serviceId.service, + name: TaggedResourceDefinition.name, + resourceIdPath: 'tag._links.resources.href', + }, + { + namespace: serviceId.service, + name: UntaggedResourceDefinition.name, + resourceIdPath: 'tag._links.resources.href', + }, + ], + }, + }, + }, + }, + ], }, { logger }, { @@ -202,9 +232,11 @@ const initializeApp = async (): Promise => { health: { href: new URL('/health', rootUrl).href }, api: [ { + name: 'directory-v2', href: new URL('/directory/v2', rootUrl).href, }, { + name: 'resource-v1', href: new URL('/resource/v1', rootUrl).href, }, ],