diff --git a/readme.md b/readme.md index a28dc2c2..49b6befd 100644 --- a/readme.md +++ b/readme.md @@ -189,6 +189,8 @@ export interface CFMAppOptions { // true if the application sets the page title // false (default) if CFM sets page title from document name appSetsWindowTitle?: boolean + // true if the content stored to file/disk should be wrapped with CFM metadata + // false if the content should be unwrapped before storing to file/disk wrapFileContent?: boolean mimeType?: string // note different capitalization from CFMBaseProviderOptions diff --git a/src/code/client.ts b/src/code/client.ts index 8945ee27..f066a034 100644 --- a/src/code/client.ts +++ b/src/code/client.ts @@ -142,7 +142,7 @@ class CloudFileManagerClient { this.appOptions = appOptions if (this.appOptions.wrapFileContent == null) { this.appOptions.wrapFileContent = true } CloudContent.wrapFileContent = this.appOptions.wrapFileContent - if (this.appOptions.isClientContent) cloudContentFactory.isClientContent = this.appOptions.isClientContent + if (this.appOptions.isClientContent) CloudContent.isClientContent = this.appOptions.isClientContent type ProviderClass = any const allProviders: Record = {} diff --git a/src/code/providers/provider-interface.test.ts b/src/code/providers/provider-interface.test.ts new file mode 100644 index 00000000..3a13bbf8 --- /dev/null +++ b/src/code/providers/provider-interface.test.ts @@ -0,0 +1,33 @@ +import { CloudContent, cloudContentFactory } from "./provider-interface" + +describe("ProviderInterface", () => { + + it("wraps/unwraps client content with `metadata` property (e.g. CODAP v2)", () => { + const docContent = { metadata: {} } + expect(CloudContent.isClientContent(docContent)).toBe(true) + const wrappedContent = cloudContentFactory.createEnvelopedCloudContent(docContent) + const unwrappedContent = wrappedContent.getClientContent() + expect(unwrappedContent).toEqual(docContent) + expect(CloudContent.isClientContent(unwrappedContent)).toBe(true) + }) + + it("can't wrap/unwrap client content with `content` property (e.g. CODAP v3) without isClientContent", () => { + const docContent = { content: { isContent: true } } + const wrappedContent = cloudContentFactory.createEnvelopedCloudContent(docContent) + const unwrappedContentFail = wrappedContent.getClientContent() + // without the isClientContent override, unwrapping fails + expect(unwrappedContentFail).not.toEqual(docContent) + }) + + it("wraps/unwraps client content with `content` property (e.g. CODAP v3) with isClientContent", () => { + const docContent = { content: { isContent: true } } + // with the isClientContent override, unwrapping succeeds + CloudContent.isClientContent = (inContent: any) => !!inContent?.content?.isContent + expect(CloudContent.isClientContent(docContent)).toBe(true) + const wrappedContent = cloudContentFactory.createEnvelopedCloudContent(docContent) + const unwrappedContent = wrappedContent.getClientContent() + expect(CloudContent.isClientContent(unwrappedContent)).toBe(true) + expect(unwrappedContent).toEqual(docContent) + }) + +}) diff --git a/src/code/providers/provider-interface.ts b/src/code/providers/provider-interface.ts index a9875993..ffb7530c 100644 --- a/src/code/providers/provider-interface.ts +++ b/src/code/providers/provider-interface.ts @@ -166,14 +166,8 @@ interface IEnvelopeMetaData { // singleton that can create CloudContent wrapped with global options class CloudContentFactory { - // For backward compatibility, by default we assume that a top-level `metadata` - // property indicates an unwrapped client document (e.g. CODAP v2). Clients can - // override this assumption with the `isClientContent` configuration option. - isClientContent = (content: unknown) => { - return typeof content === "object" && "metadata" in content && !!content.metadata - } - envelopeMetadata: IEnvelopeMetaData + constructor() { this.envelopeMetadata = { // replaced by version number at build time @@ -215,7 +209,7 @@ class CloudContentFactory { } } // If looks like client content, then it's neither wrapped nor pre-CFM. - if (this.isClientContent(content)) { + if (CloudContent.isClientContent(content)) { return result } if ( @@ -237,7 +231,7 @@ class CloudContentFactory { // noop, just checking if it's json or plain text } } - if ((typeof content === "object") && (content?.content != null)) { + if ((typeof content === "object") && (content?.content != null) && !CloudContent.isClientContent(content)) { return content } else { return {content} @@ -251,8 +245,18 @@ export interface CloudContentFormat { } class CloudContent { + // Client content is always wrapped by the CFM while it is being handled internally. + // This setting controls whether content is stored to file/disk in its wrapped form + // or whether it should be unwrapped before serializing to file/disk. static wrapFileContent: boolean = true + // For backward compatibility, by default we assume that a top-level `metadata` + // property indicates an unwrapped client document (e.g. CODAP v2). Clients can + // override this assumption with the `isClientContent` configuration option. + static isClientContent = (content: unknown) => { + return typeof content === "object" && "metadata" in content && !!content.metadata + } + // TODO: These should probably be private, but there is some refactoring // that has to happen to make this possible cfmVersion?: string @@ -264,7 +268,8 @@ class CloudContent { this.contentFormat = contentFormat } - // getContent and getContentAsJSON return the file content as stored on disk + // getContent and getContentAsJSON return the file content as stored on disk. + // They are expected to be called on internally wrapped content. getContent() { return CloudContent.wrapFileContent ? this.content @@ -275,11 +280,15 @@ class CloudContent { return JSON.stringify(this.getContent()) } - // returns the client-visible content (excluding wrapper for wrapped clients) + // Returns the client-visible content (excluding wrapper). + // Note that this can be called with wrapped or unwrapped content independent of the `wrapFileContent` + // setting, because CFM wraps content internally, so we need to inspect the content. getClientContent() { - return CloudContent.wrapFileContent - ? this.content.content - : this.content + // if we can specifically identify client content, then return it + if (CloudContent.isClientContent(this.content?.content)) return this.content.content + if (CloudContent.isClientContent(this.content)) return this.content + // otherwise, assume that a nested `content` property means we are wrapped + return this.content?.content ?? this.content } requiresConversion() {