import SDKAdapter from '../core/adapter.js' import { SDKCollection } from '../core/collection.js' import { SDKModel } from '../core/model.js' import { SDKModelSchema } from '../core/schema.js' import { nanoid } from '../core/transport.js' import librarySchema from '../schemas/library.json' assert { type: 'json' } import type { SDK } from '../sdk.js' import * as Style from '../style/index.js' import { Atleast, RequiredKeys } from '../utils/typescript.js' import { TenantModel } from './external/tenants.js' import { StylesheetModel } from './stylesheets.js' export class LibraryError extends Error { name = 'LibraryError' } /** * The NanoId identifier. * * @minLength 3 * @maxLength 64 */ export type NanoId = string /** Defines the schema for Library. */ export interface Library { /** The identifier of the Library */ id: NanoId /** * The name of the Library. * * @minLength 1 * @maxLength 128 */ name: string /** * The identifier referencing XM project library is created for. This enables additional security features that only * allow XM Users having access to that project access the library. * * @minLength 1 * @maxLength 128 */ xmProjectId?: string /** * API Key is used to access the library programmatically. * * @minLength 0 * @maxLength 64 */ apiKey?: string organizationId?: string /** * ID of a library that should be used as a template on creation. * * @minLength 0 * @maxLength 64 * @nullable */ templateId?: string | null /** Library settings */ settings?: LibrarySettings } export class Libraries extends SDKAdapter { get({ id, systemId }: Pick & { systemId: string }) { return this.sdk.fetchJSON(`/libraries/${id}${systemId ? `?systemId=${systemId}` : ''}`) } fetch({ organizationId }: Pick): Promise { return this.sdk.fetchJSON(`/libraries`) } post(library: Atleast): Promise { return this.sdk.fetchJSON(`/libraries`, { method: 'POST', body: JSON.stringify(library) }) } put(library: Partial): Promise { return this.sdk.fetchJSON(`/libraries/${library.id}`, { method: 'PUT', body: JSON.stringify(library) }) } } export class TemplateLibraries extends SDKAdapter { fetch({ organizationId }: Pick): Promise { return this.sdk.fetchJSON(`/libraries/templates`) } } export function getLibraryDefaults(sdk: SDK) { const now = new Date() return { id: nanoid(10), apiKey: nanoid(32), createdAt: now, modifiedAt: now, templateId: 'ootb-blank', settings: {} } } export type LibraryMinimal = RequiredKeys export interface LibraryParams extends Library {} export interface LibraryModel extends LibraryParams {} export interface LibrarySettings { externalPreviewURL?: string renderingHostEnv?: string templates?: string[] contentHubOneDeliveryKey?: string contentHubOnePreviewKey?: string } export class LibraryModel extends SDKModel implements LibraryParams { collections: ReturnType datasources: ReturnType contentHubOneDatasources: ReturnType stylesheets: ReturnType components: ReturnType settings: LibrarySettings get adapter() { return this.sdk.Libraries } defineProperties() { super.defineProperties() this.sdk.utils.defineCollectionAccessor(this, 'collections', this.constructCollections(), {}, this.getHiddenProps()) this.sdk.utils.defineCollectionAccessor(this, 'datasources', this.constructDatasources(), {}, this.getHiddenProps()) this.sdk.utils.defineCollectionAccessor( this, 'contentHubOneDatasources', this.constructContentHubOneDatasources(), {}, this.getHiddenProps() ) this.sdk.utils.defineCollectionAccessor(this, 'stylesheets', this.constructStylesheets(), {}, this.getHiddenProps()) this.sdk.utils.defineCollectionAccessor(this, 'components', this.constructComponents(), {}, this.getHiddenProps()) this.sdk.utils.defineAccessor(this, 'stylesheet', {}, this.getHiddenProps()) } getHiddenProps() { return ['stylesheet', 'collections', 'components', 'datasources', 'contentHubOneDatasources', 'stylesheets'] } constructCollections() { return SDKCollection.construct(this.sdk.Collection, () => ({ library: this, libraryId: this.id })) } constructComponents() { return SDKCollection.construct(this.sdk.Component, () => ({ library: this, libraryId: this.id })) } constructDatasources() { const datasources = SDKCollection.construct(this.sdk.Datasource, () => ({ library: this, libraryId: this.id, externalSourceId: this.id })) return datasources } constructContentHubOneDatasources() { const datasources = SDKCollection.construct(this.sdk.Datasource, () => ({ library: this, libraryId: this.id, type: 'contentHubOne' })) return datasources } constructStylesheets() { return SDKCollection.construct(this.sdk.Stylesheet, () => ({ library: this, libraryId: this.id })) } // Get most up to date stylesheet get stylesheet() { return this.stylesheets?.[0]?.getCurrent() } // No-op set stylesheet(stylesheet: StylesheetModel) { //throw new Error( // 'Setting library.stylesheet is deprecated. Use library.stylesheets.fetch() instead, so that library.stylesheet can be computed dynamically.' //) } getDefaults(sdk: SDK) { return getLibraryDefaults(sdk) } getPath() { return `/libraries/${this.id}` } fetchAll(includeExternalDatasources = false, includeVersions = false) { // Do not wait includeExternalDatasources && this.sdk.renderingHost.forms.fetch({ libraryId: this.id }).catch(() => {}) return Promise.all( [ this.collections .fetch() .then((collections) => Promise.all( collections .map((collection) => includeVersions ? collection.components.map((component) => component.versions.fetch()) : [] ) .flat(Infinity) ) ), this.stylesheets.fetch(), this.datasources.fetch(), includeExternalDatasources && this.contentHubOneDatasources.fetch(), includeExternalDatasources && this.sdk.auth.tenant?.datasources.fetch({ libraryId: this.id }) ] .flat(Infinity) .filter(Boolean) ) } serialize() { return { ...this.export(), collections: this.collections.map((c) => c.serialize()), stylesheets: this.stylesheets.map((c) => c.serialize()), datasources: this.datasources.map((c) => c.serialize()) } } async setXMProjectData({ authorization, systemId }: { authorization?: string; systemId: string }) { const tenants = await this.sdk.Tenants.search({ systemId: systemId, xmProjectId: this.id, authorization: authorization || `Bearer ${this.sdk.accessToken}`, limit: 1 }) if (tenants?.length) { const tenant = tenants[0] this.set({ name: TenantModel.generateProjectNameFromTenant(tenant) || this.id, xmProjectId: TenantModel.generateProjectIdFromTenant(tenant) }) } } getPreviewURL() { return this.settings.externalPreviewURL || this.sdk.renderingHost.getPreviewURL() } getFallbackPreviewURL() { return this.settings.externalPreviewURL || this.sdk.renderingHost.getFallbackPreviewURL() } /* `decorate` is a method that is used to add additional functionality to existing methods or properties of an object. In this code, it is used to add error handling and optimistic updates to various methods of the SDK classes, as well as to define new properties and accessors for the `LibraryModel` class. It is a common pattern in JavaScript and TypeScript development to use decorators to modify or extend the behavior of classes and objects. */ decorate(navigate: (to: string, options: { replace?: boolean; state?: any }) => void) { const sdk = this.sdk const library = this sdk.utils.decorate(sdk.Library.prototype, 'error', 'get', (e) => sdk.reportError(e, 'Could not fetch library')) sdk.utils.decorate(library.collections, 'success', 'fetch', function (collections) { library.components.setItems( collections.reduce((components, collection) => components.concat(collection.components), []) ) }) sdk.utils.decorate(library.collections, 'error', 'fetch', (e) => sdk.reportError(e, 'Could not fetch collections')) sdk.utils.decorate(sdk.Collection.prototype, 'success', 'put', function (collection) { library.collections.updateItem(collection) }) sdk.utils.decorate(sdk.Collection.prototype, 'error', 'put', (e) => sdk.reportError(e, 'Could not update collection') ) sdk.utils.decorate(sdk.Collection.prototype, 'success', 'post', function () { library.collections.upsertItem(this) }) sdk.utils.decorate(sdk.Collection.prototype, 'error', 'post', (e) => sdk.reportError(e, 'Could not create collection') ) // Deletion of components is optimistic sdk.utils.decorate(sdk.Collection.prototype, 'before', 'delete', function () { library.collections.removeItem(this) }) sdk.utils.decorate(sdk.Collection.prototype, 'success', 'delete', function () { library.collections.fetch() }) sdk.utils.decorate(sdk.Collection.prototype, 'error', 'delete', (e) => sdk.reportError(e, 'Could not delete collection') ) sdk.utils.decorate(sdk.Component.prototype, 'success', 'put', function () { library.components.updateItem(this) }) sdk.utils.decorate(sdk.Component.prototype, 'error', 'put', (e) => sdk.reportError(e, 'Could not update component')) sdk.utils.decorate(sdk.Component.prototype, 'success', 'post', function () { library.components.upsertItem(this) }) sdk.utils.decorate(sdk.Component.prototype, 'error', 'post', (e) => sdk.reportError(e, 'Could not create component') ) sdk.utils.decorate(library.collections, 'error', 'fetch', (e) => sdk.reportError(e, 'Could not fetch collections')) sdk.utils.decorate(sdk.Version.prototype, 'error', 'post', (e) => sdk.reportError(e, 'Could not create version')) sdk.utils.decorate(sdk.Version.prototype, 'error', 'put', (e) => sdk.reportError(e, 'Could not update version')) sdk.utils.decorate(sdk.Component.prototype, 'success', 'delete', function () { library.components.removeItem(this) }) sdk.utils.decorate(sdk.Component.prototype, 'error', 'delete', (e) => sdk.reportError(e, 'Could not delete component') ) sdk.utils.decorate(sdk.VersionBundle.prototype, 'error', 'post', (e) => sdk.reportError(e, 'Could not save bundle')) sdk.utils.decorate(sdk.VersionBundle.prototype, 'error', 'put', (e) => sdk.reportError(e, 'Could not save bundle')) sdk.utils.decorate(sdk.VersionBundle.prototype, 'error', 'delete', (e) => sdk.reportError(e, 'Could not delete bundle') ) // internal (library) datasources sdk.utils.decorate(library.datasources, 'error', 'fetch', (e) => sdk.reportError(e, 'Could not fetch datasources')) // external content hub one datasources sdk.utils.decorate(library.contentHubOneDatasources, 'error', 'fetch', (e) => { // graciously fail library.contentHubOneDatasources.setItems([]) console.log('Could not fetch ch1 datasources:', e) }) // external (tenant) datasources sdk.utils.decorate(sdk.auth.tenant.datasources, 'error', 'fetch', (e) => { // graciously fail sdk.auth.tenant.datasources.setItems([]) console.log('Could not fetch xm cloud datasources:', e) // sdk.reportError(e, 'Could not fetch datasources') }) // on changes to tenant datasources, refresh all datasources sdk.auth.tenant.datasources.observe(() => { library.onDatasourceChange() }) // on changes to library datasources, refresh all datasources library.datasources.observe(() => { library.onDatasourceChange() }) // on changes to content hub one datasources, refresh all datasources library.contentHubOneDatasources.observe(() => { library.onDatasourceChange() }) // on changes to content hub one datasources, refresh all datasources sdk.renderingHost.registeredDatasources.observe(() => { library.onDatasourceChange() }) sdk.utils.decorate(sdk.Datasource.prototype, 'success', 'put', function () { this.getCollection().upsertItem(this) }) sdk.utils.decorate(sdk.Datasource.prototype, 'error', 'put', (e) => sdk.reportError(e, 'Could not update datasource') ) sdk.utils.decorate(sdk.Datasource.prototype, 'success', 'post', function () { this.getCollection().upsertItem(this) }) sdk.utils.decorate(sdk.Datasource.prototype, 'error', 'post', (e) => sdk.reportError(e, 'Could not create component') ) sdk.utils.decorate(sdk.Datasource.prototype, 'success', 'delete', function () { this.getCollection().removeItem(this) }) sdk.utils.decorate(sdk.Datasource.prototype, 'error', 'delete', function (e) { sdk.reportError(e, 'Could not delete datasource') }) sdk.utils.decorate(sdk.Stylesheet.prototype, 'before', 'put', function () { library.stylesheets.upsertItem(this) library.onChange() }) sdk.utils.decorate(library.stylesheets, 'error', 'fetch', (e) => sdk.reportError(e, 'Could not fetch styles')) sdk.utils.decorate(sdk.Stylesheet.prototype, 'error', 'put', (e) => sdk.reportError(e, 'Could not save styles')) } static get schema(): SDKModelSchema { return new SDKModelSchema(LibraryModel, librarySchema) } onDatasourceChange() { const regularDatasources = [ ...this?.datasources, ...this?.contentHubOneDatasources, ...this.sdk.auth.tenant?.datasources ] this.sdk.datasources.setItems([ ...regularDatasources, // ignore datasource extensions ...this.sdk.renderingHost.registeredDatasources.filter((datasource) => { return datasource.sample && !regularDatasources.some((d) => d.id == datasource.id) }) ]) } }