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
14 changes: 8 additions & 6 deletions apps/opencase/src/application/case/endpoints/ImportFramework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CFRubric } from '../../../domain/case/entities/CFRubric'
import { CFPackage } from '../../../domain/case/entities/CFPackage'
import { logger } from '../../../infrastructure/logging/Logger'
import { OPENCASE_EXT_KEY, stripOpenCaseExtensions } from '../services/openCaseExtensions'
import { deriveSourcePackageURI } from '../services/packageUri'
import { coerceMistypedStrings } from '../services/coerceMistypedStrings'
import type { ImportDocumentFlags } from '../types/ImportDocumentFlags'

Expand Down Expand Up @@ -38,8 +39,8 @@ export interface ImportFrameworkResult {
* Create the `ext:opencase` extension on a CFDocument payload, marking it as a
* pristine mirror of the source. The payload must already have had any incoming
* `ext:opencase` discarded (see execute) — OpenCASE's own state is never taken
* from an import source. `endpointUrl` is only known when the framework was
* imported by URL (as opposed to a pasted JSON payload).
* from an import source. `endpointUrl` is the import URL, or for pasted JSON
* whatever source could be derived from the document (may be unknown).
*/
function injectSourceProvenance (
docPayload: any,
Expand All @@ -49,9 +50,9 @@ function injectSourceProvenance (
const existing = docPayload.extensions ?? {}

const extPatch: Record<string, unknown> = {
// Only record a source URL when one is known — pasted JSON has no
// publisher endpoint, so the field is left off entirely rather than
// written as undefined.
// Only record a source URL when one is known — pasted JSON whose source
// can't be derived has none, so the field is left off entirely rather
// than written as undefined.
...(endpointUrl ? { sourcePackageURI: endpointUrl } : {}),
isModifiedFromSource: false,
importedAt: new Date().toISOString(),
Expand Down Expand Up @@ -111,7 +112,8 @@ export class ImportFramework {
} else {
logger.info({ tenantId, caseVersion }, 'Importing framework from provided JSON')
const normalized = stripOpenCaseExtensions(normalizeCfPackageData(cfPackage))
const provenanceUri = (sourcePackageUri ?? endpointUrl ?? '').trim()
// With no import endpoint, fall back to the source the document itself points at.
const provenanceUri = (sourcePackageUri ?? endpointUrl ?? deriveSourcePackageURI(normalized.CFDocument) ?? '').trim()
sourceCFPackage = {
...normalized,
CFDocument: injectSourceProvenance(normalized.CFDocument, provenanceUri || undefined, documentFlags)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ describe('ImportFramework', () => {
expect(mockRepository.saveNewVersion).toHaveBeenCalledTimes(1)
})

it('derives the source CFPackage URL for pasted JSON from the document uri (CGE-45)', async () => {
await importFramework.execute({
tenantId,
caseVersion,
cfPackage: {
CFPackage: { CFDocument: { ...cfDocument, uri: 'https://source.example.org/ims/case/v1p1/CFDocuments/doc-123' } }
}
})

const savedPkg = mockRepository.saveNewVersion.mock.calls[0][2]
const savedExtensions = savedPkg.document.toJSON().extensions
expect(savedExtensions['ext:opencase'].sourcePackageURI).toBe('https://source.example.org/ims/case/v1p1/CFPackages/doc-123')
expect(savedExtensions['ext:opencase'].isModifiedFromSource).toBe(false)
})

it('marks pasted JSON as a pristine mirror without a sourcePackageURI (no known source URL)', async () => {
await importFramework.execute({
tenantId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildCFPackageURI, getOpenCaseExtension } from '../packageUri'
import { buildCFPackageURI, deriveSourcePackageURI, getOpenCaseExtension } from '../packageUri'

describe('buildCFPackageURI (CGE-59)', () => {
const source = 'https://standards.example.org/ims/case/v1p1/CFPackages/doc-1'
Expand All @@ -23,6 +23,29 @@ describe('buildCFPackageURI (CGE-59)', () => {
})
})

describe('deriveSourcePackageURI (CGE-45)', () => {
it('uses the CFPackageURI the publisher declared on the CFDocument', () => {
expect(deriveSourcePackageURI({
uri: 'https://a.example.org/ims/case/v1p1/CFDocuments/doc-1',
CFPackageURI: { uri: 'https://b.example.org/ims/case/v1p1/CFPackages/doc-1' }
})).toBe('https://b.example.org/ims/case/v1p1/CFPackages/doc-1')
})

it('derives the CFPackages URL from a CASE API CFDocument uri, keeping any path prefix', () => {
expect(deriveSourcePackageURI({ uri: 'https://case.example.org/ims/case/v1p1/CFDocuments/doc-1' }))
.toBe('https://case.example.org/ims/case/v1p1/CFPackages/doc-1')
expect(deriveSourcePackageURI({ uri: 'https://example.org/api/ims/case/v1p0/CFDocuments/doc-1' }))
.toBe('https://example.org/api/ims/case/v1p0/CFPackages/doc-1')
})

it('returns undefined when no source can be determined', () => {
expect(deriveSourcePackageURI({ uri: '/ims/case/v1p1/CFDocuments/doc-1' })).toBeUndefined()
expect(deriveSourcePackageURI({ uri: 'https://example.org/uri/doc-1' })).toBeUndefined()
expect(deriveSourcePackageURI({ uri: 'urn:case:doc-1', CFPackageURI: { uri: 'urn:case:package:doc-1' } })).toBeUndefined()
expect(deriveSourcePackageURI(undefined)).toBeUndefined()
})
})

describe('getOpenCaseExtension', () => {
it('returns the ext:opencase object when present', () => {
expect(getOpenCaseExtension({ 'ext:opencase': { a: 1 } })).toEqual({ a: 1 })
Expand Down
25 changes: 23 additions & 2 deletions apps/opencase/src/application/case/services/packageUri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import type { LinkData } from '../../../domain/case/value-objects/LinkData'
* It is never stored — a CFPackage's CFDocument (CFPckgDocument) has no
* CFPackageURI, so it only exists in these responses.
*
* - Pristine mirror imported by URL: points at the source it was imported from.
* - Pristine mirror with a known source (imported by URL, or pasted as JSON
* whose source could be derived — see deriveSourcePackageURI): points at that source.
* - Everything else (forks, locally authored, mirrors pasted as JSON with no
* known source): points at this server's CFPackages endpoint.
* derivable source): points at this server's CFPackages endpoint.
*/
export function buildCFPackageURI (
docId: SourcedId,
Expand All @@ -23,6 +24,26 @@ export function buildCFPackageURI (
return { title: 'CFPackage', identifier: docId, uri }
}

/**
* Work out the publisher's CFPackage URL for a framework pasted/uploaded as JSON,
* where there's no import endpoint to record. Uses the CFDocument's own
* CFPackageURI if the publisher included one, otherwise derives it from a
* CASE-API-shaped CFDocument uri (`…/ims/case/v1pX/CFDocuments/{id}` →
* `…/ims/case/v1pX/CFPackages/{id}`). Only absolute http(s) URLs are accepted;
* returns undefined when no source can be determined.
*/
export function deriveSourcePackageURI (cfDocument: any): string | undefined {
const isAbsoluteHttp = (v: unknown): v is string => typeof v === 'string' && /^https?:\/\//i.test(v.trim())

const declared = cfDocument?.CFPackageURI?.uri
if (isAbsoluteHttp(declared)) return declared.trim()

const docUri = cfDocument?.uri
if (!isAbsoluteHttp(docUri)) return undefined
const match = docUri.trim().match(/^(.*\/ims\/case\/v1p[01])\/CFDocuments\/([^/?#]+)\/?$/i)
return match ? `${match[1]}/CFPackages/${match[2]}` : undefined
}

/** Read the `ext:opencase` object off a CFDocument's extensions, if present. */
export function getOpenCaseExtension (extensions: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
const ext = extensions?.['ext:opencase']
Expand Down
Loading