diff --git a/apps/opencase/src/application/case/endpoints/GetCFConcept.ts b/apps/opencase/src/application/case/endpoints/GetCFConcept.ts index c2f6f5c..f3f51c7 100644 --- a/apps/opencase/src/application/case/endpoints/GetCFConcept.ts +++ b/apps/opencase/src/application/case/endpoints/GetCFConcept.ts @@ -18,7 +18,18 @@ export class GetCFConcept { async execute (query: GetCFConceptQuery) { const entry = this.store.getDefinitionById(query.tenantId, query.caseVersion, 'CFConcepts', query.sourcedId) if (!entry) return null - return entry.value + + // Per spec, the response is the CFConcept plus the set of children as + // determined by their place in the 'hierarchyCode' of the CFConcept. + const hierarchyCode = entry.value?.hierarchyCode as string | undefined + const allConcepts = this.store.getTenantDefinitions(query.tenantId, query.caseVersion).CFConcepts + const children = hierarchyCode + ? allConcepts + .filter((c: any) => c.identifier !== entry.value.identifier && typeof c.hierarchyCode === 'string' && c.hierarchyCode.startsWith(`${hierarchyCode}.`)) + .sort((a: any, b: any) => a.hierarchyCode.localeCompare(b.hierarchyCode)) + : [] + + return { CFConcepts: [entry.value, ...children] } } } diff --git a/apps/opencase/src/application/case/endpoints/GetCFItemType.ts b/apps/opencase/src/application/case/endpoints/GetCFItemType.ts index 2e66dfc..d97abaf 100644 --- a/apps/opencase/src/application/case/endpoints/GetCFItemType.ts +++ b/apps/opencase/src/application/case/endpoints/GetCFItemType.ts @@ -18,7 +18,18 @@ export class GetCFItemType { async execute (query: GetCFItemTypeQuery) { const entry = this.store.getDefinitionById(query.tenantId, query.caseVersion, 'CFItemTypes', query.sourcedId) if (!entry) return null - return entry.value + + // Per spec, the response is the CFItemType plus the set of children as + // determined by their place in the 'hierarchyCode' of the CFItemType. + const hierarchyCode = entry.value?.hierarchyCode as string | undefined + const allItemTypes = this.store.getTenantDefinitions(query.tenantId, query.caseVersion).CFItemTypes + const children = hierarchyCode + ? allItemTypes + .filter((t: any) => t.identifier !== entry.value.identifier && typeof t.hierarchyCode === 'string' && t.hierarchyCode.startsWith(`${hierarchyCode}.`)) + .sort((a: any, b: any) => a.hierarchyCode.localeCompare(b.hierarchyCode)) + : [] + + return { CFItemTypes: [entry.value, ...children] } } } diff --git a/apps/opencase/src/application/case/endpoints/GetCFSubject.ts b/apps/opencase/src/application/case/endpoints/GetCFSubject.ts index c33e479..2c6026e 100644 --- a/apps/opencase/src/application/case/endpoints/GetCFSubject.ts +++ b/apps/opencase/src/application/case/endpoints/GetCFSubject.ts @@ -18,7 +18,18 @@ export class GetCFSubject { async execute (query: GetCFSubjectQuery) { const entry = this.store.getDefinitionById(query.tenantId, query.caseVersion, 'CFSubjects', query.sourcedId) if (!entry) return null - return entry.value + + // Per spec, the response is the CFSubject plus the set of children as + // determined by their place in the 'hierarchyCode' of the CFSubject. + const hierarchyCode = entry.value?.hierarchyCode as string | undefined + const allSubjects = this.store.getTenantDefinitions(query.tenantId, query.caseVersion).CFSubjects + const children = hierarchyCode + ? allSubjects + .filter((s: any) => s.identifier !== entry.value.identifier && typeof s.hierarchyCode === 'string' && s.hierarchyCode.startsWith(`${hierarchyCode}.`)) + .sort((a: any, b: any) => a.hierarchyCode.localeCompare(b.hierarchyCode)) + : [] + + return { CFSubjects: [entry.value, ...children] } } } diff --git a/apps/opencase/src/application/case/endpoints/__tests__/GetCFConcept.test.ts b/apps/opencase/src/application/case/endpoints/__tests__/GetCFConcept.test.ts index 010df64..1758b72 100644 --- a/apps/opencase/src/application/case/endpoints/__tests__/GetCFConcept.test.ts +++ b/apps/opencase/src/application/case/endpoints/__tests__/GetCFConcept.test.ts @@ -14,7 +14,8 @@ describe('GetCFConcept', () => { } as any mockStore = { - getDefinitionById: jest.fn() + getDefinitionById: jest.fn(), + getTenantDefinitions: jest.fn() } as any getCFConcept = new GetCFConcept(mockRepository, mockStore) @@ -34,10 +35,11 @@ describe('GetCFConcept', () => { expect(mockStore.getDefinitionById).toHaveBeenCalledWith(tenantId, caseVersion, 'CFConcepts', conceptId) }) - it('should return CFConcept when found', async () => { + it('should return a CFConceptSet containing just the concept when it has no children', async () => { const concept = { identifier: conceptId, title: 'Test Concept', + hierarchyCode: '1.01', uri: '/ims/case/v1p1/CFConcepts/concept-123' } @@ -45,12 +47,30 @@ describe('GetCFConcept', () => { docSourcedId: 'doc-123', value: concept } as any) + mockStore.getTenantDefinitions.mockReturnValue({ CFConcepts: [concept] } as any) const result = await getCFConcept.execute({ tenantId, caseVersion, sourcedId: conceptId }) - expect(result).toEqual(concept) + expect(result).toEqual({ CFConcepts: [concept] }) expect(mockStore.getDefinitionById).toHaveBeenCalledWith(tenantId, caseVersion, 'CFConcepts', conceptId) }) + + it('should include children determined by hierarchyCode, sorted, excluding unrelated siblings', async () => { + const concept = { identifier: conceptId, title: 'Parent', hierarchyCode: '1.01' } + const childB = { identifier: 'child-b', title: 'Child B', hierarchyCode: '1.01.02' } + const childA = { identifier: 'child-a', title: 'Child A', hierarchyCode: '1.01.01' } + const unrelated = { identifier: 'other', title: 'Unrelated', hierarchyCode: '1.02' } + const falsePrefixMatch = { identifier: 'not-a-child', title: 'Not a child', hierarchyCode: '1.010' } + + mockStore.getDefinitionById.mockReturnValue({ docSourcedId: 'doc-123', value: concept } as any) + mockStore.getTenantDefinitions.mockReturnValue({ + CFConcepts: [concept, childB, childA, unrelated, falsePrefixMatch] + } as any) + + const result = await getCFConcept.execute({ tenantId, caseVersion, sourcedId: conceptId }) + + expect(result).toEqual({ CFConcepts: [concept, childA, childB] }) + }) }) }) diff --git a/apps/opencase/src/application/case/endpoints/__tests__/GetCFItemType.test.ts b/apps/opencase/src/application/case/endpoints/__tests__/GetCFItemType.test.ts index 959e8dd..ed0609b 100644 --- a/apps/opencase/src/application/case/endpoints/__tests__/GetCFItemType.test.ts +++ b/apps/opencase/src/application/case/endpoints/__tests__/GetCFItemType.test.ts @@ -14,7 +14,8 @@ describe('GetCFItemType', () => { } as any mockStore = { - getDefinitionById: jest.fn() + getDefinitionById: jest.fn(), + getTenantDefinitions: jest.fn() } as any getCFItemType = new GetCFItemType(mockRepository, mockStore) @@ -34,7 +35,7 @@ describe('GetCFItemType', () => { expect(mockStore.getDefinitionById).toHaveBeenCalledWith(tenantId, caseVersion, 'CFItemTypes', itemTypeId) }) - it('should return CFItemType when found', async () => { + it('should return a CFItemTypeSet containing just the item type when it has no children', async () => { const itemType = { identifier: itemTypeId, title: 'Test Item Type', @@ -49,12 +50,29 @@ describe('GetCFItemType', () => { value: itemType, lastChangeDateTime: '2024-01-01T00:00:00.000Z' } as any) + mockStore.getTenantDefinitions.mockReturnValue({ CFItemTypes: [itemType] } as any) const result = await getCFItemType.execute({ tenantId, caseVersion, sourcedId: itemTypeId }) - expect(result).toEqual(itemType) + expect(result).toEqual({ CFItemTypes: [itemType] }) expect(mockStore.getDefinitionById).toHaveBeenCalledWith(tenantId, caseVersion, 'CFItemTypes', itemTypeId) }) + + it('should include children determined by hierarchyCode, sorted', async () => { + const itemType = { identifier: itemTypeId, title: 'Parent', hierarchyCode: '01' } + const childB = { identifier: 'child-b', title: 'Child B', hierarchyCode: '01.02' } + const childA = { identifier: 'child-a', title: 'Child A', hierarchyCode: '01.01' } + const unrelated = { identifier: 'other', title: 'Unrelated', hierarchyCode: '02' } + + mockStore.getDefinitionById.mockReturnValue({ docSourcedId: 'doc-123', value: itemType } as any) + mockStore.getTenantDefinitions.mockReturnValue({ + CFItemTypes: [itemType, childB, childA, unrelated] + } as any) + + const result = await getCFItemType.execute({ tenantId, caseVersion, sourcedId: itemTypeId }) + + expect(result).toEqual({ CFItemTypes: [itemType, childA, childB] }) + }) }) }) diff --git a/apps/opencase/src/application/case/endpoints/__tests__/GetCFSubject.test.ts b/apps/opencase/src/application/case/endpoints/__tests__/GetCFSubject.test.ts index 8b71076..8225ec7 100644 --- a/apps/opencase/src/application/case/endpoints/__tests__/GetCFSubject.test.ts +++ b/apps/opencase/src/application/case/endpoints/__tests__/GetCFSubject.test.ts @@ -14,7 +14,8 @@ describe('GetCFSubject', () => { } as any mockStore = { - getDefinitionById: jest.fn() + getDefinitionById: jest.fn(), + getTenantDefinitions: jest.fn() } as any getCFSubject = new GetCFSubject(mockRepository, mockStore) @@ -34,10 +35,11 @@ describe('GetCFSubject', () => { expect(mockStore.getDefinitionById).toHaveBeenCalledWith(tenantId, caseVersion, 'CFSubjects', subjectId) }) - it('should return CFSubject when found', async () => { + it('should return a CFSubjectSet containing just the subject when it has no children', async () => { const subject = { identifier: subjectId, title: 'Mathematics', + hierarchyCode: '1.01', uri: '/ims/case/v1p1/CFSubjects/subject-123' } @@ -45,12 +47,29 @@ describe('GetCFSubject', () => { docSourcedId: 'doc-123', value: subject } as any) + mockStore.getTenantDefinitions.mockReturnValue({ CFSubjects: [subject] } as any) const result = await getCFSubject.execute({ tenantId, caseVersion, sourcedId: subjectId }) - expect(result).toEqual(subject) + expect(result).toEqual({ CFSubjects: [subject] }) expect(mockStore.getDefinitionById).toHaveBeenCalledWith(tenantId, caseVersion, 'CFSubjects', subjectId) }) + + it('should include children determined by hierarchyCode, sorted', async () => { + const subject = { identifier: subjectId, title: 'Parent', hierarchyCode: '1.01' } + const childB = { identifier: 'child-b', title: 'Child B', hierarchyCode: '1.01.02' } + const childA = { identifier: 'child-a', title: 'Child A', hierarchyCode: '1.01.01' } + const unrelated = { identifier: 'other', title: 'Unrelated', hierarchyCode: '1.02' } + + mockStore.getDefinitionById.mockReturnValue({ docSourcedId: 'doc-123', value: subject } as any) + mockStore.getTenantDefinitions.mockReturnValue({ + CFSubjects: [subject, childB, childA, unrelated] + } as any) + + const result = await getCFSubject.execute({ tenantId, caseVersion, sourcedId: subjectId }) + + expect(result).toEqual({ CFSubjects: [subject, childA, childB] }) + }) }) }) diff --git a/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFConceptsController.test.ts b/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFConceptsController.test.ts index 59605d1..b1bc207 100644 --- a/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFConceptsController.test.ts +++ b/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFConceptsController.test.ts @@ -41,12 +41,14 @@ describe('CFConceptsControllerV1p1', () => { }) describe('getById', () => { - it('should return CFConcept when found', async () => { + it('should return CFConceptSet when found', async () => { const result = { - identifier: 'concept-123', - uri: '/ims/case/v1p1/CFConcepts/concept-123', - title: 'Test Concept', - lastChangeDateTime: '2024-01-01T00:00:00.000Z' + CFConcepts: [{ + identifier: 'concept-123', + uri: '/ims/case/v1p1/CFConcepts/concept-123', + title: 'Test Concept', + lastChangeDateTime: '2024-01-01T00:00:00.000Z' + }] } mockGetCFConcept.execute.mockResolvedValue(result) diff --git a/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFItemTypesController.test.ts b/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFItemTypesController.test.ts index f9908bb..6fb7d2c 100644 --- a/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFItemTypesController.test.ts +++ b/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFItemTypesController.test.ts @@ -41,14 +41,16 @@ describe('CFItemTypesControllerV1p1', () => { }) describe('getById', () => { - it('should return CFItemType when found', async () => { + it('should return CFItemTypeSet when found', async () => { const result = { - identifier: 'itemtype-123', - uri: '/ims/case/v1p1/CFItemTypes/itemtype-123', - title: 'Test Item Type', - description: 'Test Description', - hierarchyCode: '01', - lastChangeDateTime: '2024-01-01T00:00:00.000Z' + CFItemTypes: [{ + identifier: 'itemtype-123', + uri: '/ims/case/v1p1/CFItemTypes/itemtype-123', + title: 'Test Item Type', + description: 'Test Description', + hierarchyCode: '01', + lastChangeDateTime: '2024-01-01T00:00:00.000Z' + }] } mockGetCFItemType.execute.mockResolvedValue(result) diff --git a/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFSubjectsController.test.ts b/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFSubjectsController.test.ts index 7a62199..b0cb16a 100644 --- a/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFSubjectsController.test.ts +++ b/apps/opencase/src/interfaces/http/http-public/v1p1/controllers/__tests__/CFSubjectsController.test.ts @@ -41,12 +41,14 @@ describe('CFSubjectsControllerV1p1', () => { }) describe('getById', () => { - it('should return CFSubject when found', async () => { + it('should return CFSubjectSet when found', async () => { const result = { - identifier: 'subject-123', - uri: '/ims/case/v1p1/CFSubjects/subject-123', - title: 'Mathematics', - lastChangeDateTime: '2024-01-01T00:00:00.000Z' + CFSubjects: [{ + identifier: 'subject-123', + uri: '/ims/case/v1p1/CFSubjects/subject-123', + title: 'Mathematics', + lastChangeDateTime: '2024-01-01T00:00:00.000Z' + }] } mockGetCFSubject.execute.mockResolvedValue(result)