Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions apps/opencase/src/application/case/endpoints/GetAllCFDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface GetAllCFDocumentsQuery {
filter?: string
fields?: string[]
includeArchived?: boolean
/** When true, include OpenCASE-proprietary fields derived from ext:opencase (e.g. sourcePackageURI, isModifiedFromSource). Set when the request carries the X-CASE-EDITOR header. */
includeOpenCaseExtensions?: boolean
}

export class GetAllCFDocuments {
Expand Down Expand Up @@ -124,12 +126,16 @@ export class GetAllCFDocuments {
if (docMeta.subject) doc.subject = docMeta.subject
if (docMeta.version) doc.version = docMeta.version
if (docMeta.adoptionStatus) doc.adoptionStatus = docMeta.adoptionStatus
if (docMeta.sourcePackageURI) doc.sourcePackageURI = docMeta.sourcePackageURI
// isModifiedFromSource is set (true or false) on any mirrored/forked framework,
// even one imported without a known sourcePackageURI (e.g. pasted JSON) — so it
// must be surfaced even when false, since the frontend uses its presence to
// decide whether to show the Mirrored/Forked badge at all.
if (docMeta.isModifiedFromSource !== undefined) doc.isModifiedFromSource = docMeta.isModifiedFromSource
// sourcePackageURI/isModifiedFromSource are derived from the ext:opencase extension and
// are OpenCASE-proprietary — only surface them to callers that requested extensions.
if (query.includeOpenCaseExtensions) {
if (docMeta.sourcePackageURI) doc.sourcePackageURI = docMeta.sourcePackageURI
// isModifiedFromSource is set (true or false) on any mirrored/forked framework,
// even one imported without a known sourcePackageURI (e.g. pasted JSON) — so it
// must be surfaced even when false, since the frontend uses its presence to
// decide whether to show the Mirrored/Forked badge at all.
if (docMeta.isModifiedFromSource !== undefined) doc.isModifiedFromSource = docMeta.isModifiedFromSource
}
if (docMeta.archived) doc.archived = true

// CASE v1.1-only fields: only include when not serving via v1p0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface ListFrameworksQuery {
frameworkType?: string
/** When set, only alignment frameworks listing this docId as a participant are returned */
participantId?: string
/** When true, include OpenCASE-proprietary fields derived from ext:opencase (e.g. alignmentParticipants). Set when the request carries the X-CASE-EDITOR header. */
includeOpenCaseExtensions?: boolean
}

export class ListFrameworks {
Expand Down Expand Up @@ -50,7 +52,9 @@ export class ListFrameworks {
subject: doc.subject,
version: doc.version,
lastChangeDateTime: doc.lastChangeDateTime.toISOString(),
alignmentParticipants: doc.alignmentParticipants,
// alignmentParticipants is derived from the ext:opencase extension and is
// OpenCASE-proprietary — only surface it to callers that requested extensions.
...(query.includeOpenCaseExtensions && doc.alignmentParticipants ? { alignmentParticipants: doc.alignmentParticipants } : {})
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,46 @@ describe('GetAllCFDocuments', () => {

expect(result.CFDocuments).toHaveLength(2)
})

it('should omit sourcePackageURI and isModifiedFromSource when includeOpenCaseExtensions is not set', async () => {
const documents = [
{
sourcedId: 'doc-1',
title: 'Mirrored Document',
lastChangeDateTime: new Date('2024-01-01T00:00:00Z'),
currentFile: 'file1.json',
sourcePackageURI: 'https://standards.example.org/ims/case/v1p1/CFPackages/abc',
isModifiedFromSource: true
}
]

mockStore.getAllDocuments.mockImplementation((_: any, v: any) => (v === '1.0' ? documents as any : []))

const result = await getAllCFDocuments.execute({ tenantId, caseVersion })

expect(result.CFDocuments[0]).not.toHaveProperty('sourcePackageURI')
expect(result.CFDocuments[0]).not.toHaveProperty('isModifiedFromSource')
})

it('should include sourcePackageURI and isModifiedFromSource when includeOpenCaseExtensions is true', async () => {
const documents = [
{
sourcedId: 'doc-1',
title: 'Mirrored Document',
lastChangeDateTime: new Date('2024-01-01T00:00:00Z'),
currentFile: 'file1.json',
sourcePackageURI: 'https://standards.example.org/ims/case/v1p1/CFPackages/abc',
isModifiedFromSource: false
}
]

mockStore.getAllDocuments.mockImplementation((_: any, v: any) => (v === '1.0' ? documents as any : []))

const result = await getAllCFDocuments.execute({ tenantId, caseVersion, includeOpenCaseExtensions: true })

expect(result.CFDocuments[0].sourcePackageURI).toBe('https://standards.example.org/ims/case/v1p1/CFPackages/abc')
expect(result.CFDocuments[0].isModifiedFromSource).toBe(false)
})
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,47 @@ describe('ListFrameworks', () => {

expect(result.frameworks).toHaveLength(2)
})

it('should omit alignmentParticipants when includeOpenCaseExtensions is not set', async () => {
const docs: DocumentMetadata[] = [
{
sourcedId: 'doc-1',
title: 'Alignment Framework',
lastChangeDateTime: new Date('2024-01-01T00:00:00Z'),
currentFile: 'frameworks/doc-1/doc-1_v0001.json',
alignmentParticipants: [{ identifier: 'participant-1', uri: '/ims/case/v1p1/CFDocuments/participant-1' }]
}
]

mockStore.getAllDocuments
.mockReturnValueOnce([]) // For 1.0
.mockReturnValueOnce(docs) // For 1.1

const result = await listFrameworks.execute({ tenantId })

expect(result.frameworks[0]).not.toHaveProperty('alignmentParticipants')
})

it('should include alignmentParticipants when includeOpenCaseExtensions is true', async () => {
const participants = [{ identifier: 'participant-1', uri: '/ims/case/v1p1/CFDocuments/participant-1' }]
const docs: DocumentMetadata[] = [
{
sourcedId: 'doc-1',
title: 'Alignment Framework',
lastChangeDateTime: new Date('2024-01-01T00:00:00Z'),
currentFile: 'frameworks/doc-1/doc-1_v0001.json',
alignmentParticipants: participants
}
]

mockStore.getAllDocuments
.mockReturnValueOnce([]) // For 1.0
.mockReturnValueOnce(docs) // For 1.1

const result = await listFrameworks.execute({ tenantId, includeOpenCaseExtensions: true })

expect(result.frameworks[0].alignmentParticipants).toEqual(participants)
})
})
})

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export class CFPackagesManagementController {
const frameworkType = typeof req.query.frameworkType === 'string' ? req.query.frameworkType : undefined
const participantId = typeof req.query.participantId === 'string' ? req.query.participantId : undefined

const result = await this.listFrameworks.execute({ tenantId, caseVersion, includeArchived, frameworkType, participantId })
const includeOpenCaseExtensions = req.header('X-CASE-EDITOR') !== undefined
const result = await this.listFrameworks.execute({ tenantId, caseVersion, includeArchived, frameworkType, participantId, includeOpenCaseExtensions })
return res.status(200).json(result)
} catch (error: any) {
return res.status(400).json({ error: error.message || 'List failed' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ describe('CFPackagesManagementController', () => {
mockRequest = {
params: { tenantId: 'test-tenant' },
query: {},
body: {}
}
body: {},
header: jest.fn().mockReturnValue(undefined)
} as any

mockResponse = {
status: responseStatus,
Expand All @@ -61,11 +62,26 @@ describe('CFPackagesManagementController', () => {
expect(mockListFrameworks.execute).toHaveBeenCalledWith({
tenantId: 'test-tenant',
caseVersion: '1.0',
includeArchived: false
includeArchived: false,
includeOpenCaseExtensions: false
})
expect(responseStatus).toHaveBeenCalledWith(200)
})

it('requests OpenCASE extensions when X-CASE-EDITOR header is present', async () => {
;(mockRequest as any).tenantId = 'test-tenant'
;(mockRequest.header as jest.Mock).mockImplementation((name: string) =>
name === 'X-CASE-EDITOR' ? 'true' : undefined
)
mockListFrameworks.execute.mockResolvedValueOnce({ frameworks: [], total: 0, tenantId: 'test-tenant' } as any)

await (controller.list as any)(mockRequest as Request, mockResponse as Response, next)

expect(mockListFrameworks.execute).toHaveBeenCalledWith(
expect.objectContaining({ includeOpenCaseExtensions: true })
)
})

it('archives a CFPackage by id (soft delete by default)', async () => {
;(mockRequest as any).tenantId = 'test-tenant'
mockRequest.params = { tenantId: 'test-tenant', id: 'doc-1' } as any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export class GetAllCFDocumentsControllerV1p0 {
orderBy,
filter,
fields,
includeArchived
includeArchived,
includeOpenCaseExtensions: wantsOpenCaseExtensions(req)
})

// Access control: unauthenticated requests only see public frameworks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export class GetAllCFDocumentsControllerV1p1 {
orderBy,
filter,
fields,
includeArchived
includeArchived,
includeOpenCaseExtensions: wantsOpenCaseExtensions(req)
})

// Access control: unauthenticated requests only see public frameworks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ describe('GetAllCFDocumentsControllerV1p1', () => {
orderBy: undefined,
filter: undefined,
fields: undefined,
includeArchived: false
includeArchived: false,
includeOpenCaseExtensions: false
})
expect(responseStatus).toHaveBeenCalledWith(200)
expect(responseJson).toHaveBeenCalledWith(absolutizeCaseUris(result as any, 'http://localhost'))
Expand Down Expand Up @@ -102,11 +103,28 @@ describe('GetAllCFDocumentsControllerV1p1', () => {
orderBy: 'asc',
filter: 'test',
fields: ['title', 'identifier'],
includeArchived: false
includeArchived: false,
includeOpenCaseExtensions: false
})
expect(responseStatus).toHaveBeenCalledWith(200)
})

it('should pass includeOpenCaseExtensions=true when X-CASE-EDITOR header is present', async () => {
const result = { CFDocuments: [] }
mockGetAllCFDocuments.execute.mockResolvedValue(result)
;(mockRequest as any).tenantId = 'test-tenant'
;(mockRequest as any).isAuthenticated = true
;(mockRequest.header as jest.Mock).mockImplementation((name: string) =>
name === 'X-CASE-EDITOR' ? 'true' : undefined
)

await controller.getAll(mockRequest as Request, mockResponse as Response)

expect(mockGetAllCFDocuments.execute).toHaveBeenCalledWith(
expect.objectContaining({ includeOpenCaseExtensions: true })
)
})

it('should return 400 for invalid limit', async () => {
;(mockRequest as any).tenantId = 'test-tenant'
mockRequest.query = { limit: '-1' }
Expand Down
Loading