UNPKG

scrivito

Version:

Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.

473 lines (391 loc) 12.2 kB
import mapValues from 'lodash-es/mapValues'; import { ClientError } from 'scrivito_sdk/client'; import { ArgumentError, InternalError, extractFromIterator, transformContinueIterable, } from 'scrivito_sdk/common'; import { deserializeDataAttribute, serializeDataAttribute, } from 'scrivito_sdk/data_integration/attribute_serialization_and_deserialization'; import { DataClass, DataItem, DataItemAttributes, DataScope, DataScopeError, DataScopeParams, NormalizedDataScopeParams, PresentDataScopePojo, assertValidDataItemAttributes, combineFilters, combineSearches, itemIdFromFilters, } from 'scrivito_sdk/data_integration/data_class'; import { DataAttributeDefinitions, getDataAttributeDefinitions, } from 'scrivito_sdk/data_integration/data_class_schema'; import { DataConnectionError } from 'scrivito_sdk/data_integration/data_connection_error'; import { getExternalData, setExternalData, } from 'scrivito_sdk/data_integration/external_data'; import { ExternalCustomAttributes, NormalExternalData, createViaDataConnection, deleteViaDataConnection, getExternalDataConnectionNames, hasExternalDataConnection, updateViaDataConnection, } from 'scrivito_sdk/data_integration/external_data_connection'; import { countExternalData, getExternalDataQuery, notifyExternalDataWrite, } from 'scrivito_sdk/data_integration/external_data_query'; import { load } from 'scrivito_sdk/loadable'; /** @beta */ export class ExternalDataClass extends DataClass { constructor(private readonly _name: string) { super(); } name(): string { return this._name; } async create(attributes: DataItemAttributes): Promise<DataItem> { return this.all().create(attributes); } all(): DataScope { return new ExternalDataScope(this); } get(id: string): DataItem | null { return getExternalData(this._name, id) ? ExternalDataItem.build(this, id) : null; } getUnchecked(id: string): ExternalDataItem { return ExternalDataItem.buildUnchecked(this, id); } } export function isExternalDataClassProvided(name: string): boolean { return hasExternalDataConnection(name); } export function allExternalDataClasses(): ExternalDataClass[] { return getExternalDataConnectionNames().map( (name) => new ExternalDataClass(name) ); } /** @beta */ export class ExternalDataScope extends DataScope { constructor( private readonly _dataClass: DataClass, private readonly _attributeName?: string, private readonly _params: NormalizedDataScopeParams = {} ) { super(); } dataClass(): DataClass { return this._dataClass; } dataClassName(): string { return this._dataClass.name(); } async create(attributes: DataItemAttributes): Promise<DataItem> { this.assertNoIdFilter(); assertValidDataItemAttributes(attributes); const { filters } = this._params; const dataClassName = this.dataClassName(); const dataAttributeDefinitions = await loadAttributesOrThrow(dataClassName); const serializedAttributes = serializeAttributes( dataClassName, attributes, dataAttributeDefinitions ); const dataForCallback = { ...serializedAttributes, ...this.attributesFromFilters(filters), }; const data = await createViaDataConnection(dataClassName, dataForCallback); const id = data.systemData._id; setExternalData(dataClassName, id, data); notifyExternalDataWrite(dataClassName); return ExternalDataItem.buildWithLoadedAttributes( this._dataClass, id, dataAttributeDefinitions ); } get(id: string): DataItem | null { const { filters, search } = this._params; if (!search && !filters) return this._dataClass.get(id); const idFromFilters = this.itemIdFromFilters(); if (idFromFilters && this.hasSingleFilter()) { return idFromFilters === id ? this._dataClass.get(id) : null; } return this.transform({ filters: { _id: id }, limit: 1 }).take()[0] || null; } dataItem(): DataItem | null { const id = this.itemIdFromFilters(); return id ? this.get(id) : null; } isDataItem(): boolean { return !!this.itemIdFromFilters(); } attributeName(): string | null { return this._attributeName || null; } take(): DataItem[] { const attributes = getDataAttributeDefinitions(this.dataClassName()); if (!attributes) return []; const id = this.itemIdFromFilters(); if (id && this.hasSingleFilter()) { if (this.limit() === 0) return []; const item = this.get(id); return item ? [item] : []; } return handleCommunicationError(() => this.takeUnsafe(attributes)); } transform({ filters, search, order, limit, attributeName, }: DataScopeParams): DataScope { return new ExternalDataScope( this._dataClass, attributeName || this._attributeName, { filters: combineFilters( this._params.filters, this.normalizeFilters(filters) ), search: combineSearches(this._params.search, search), order: order || this._params.order, limit: limit ?? this._params.limit, } ); } objSearch(): undefined { return; } count(): number | null { const { filters, search } = this._params; const dataClassName = this.dataClassName(); const attributes = getDataAttributeDefinitions(dataClassName); if (!attributes) return null; return handleCommunicationError(() => countExternalData(dataClassName, filters, search, attributes) ); } limit(): number | undefined { return this._params.limit; } /** @internal */ toPojo(): PresentDataScopePojo { return { _class: this.dataClassName(), _attribute: this._attributeName, ...this._params, }; } private takeUnsafe(attributes: DataAttributeDefinitions) { return extractFromIterator( this.getIterator(attributes), this._params.limit ); } private getIterator(attributes: DataAttributeDefinitions) { return transformContinueIterable( getExternalDataQuery(this.toPojo(), attributes), (iterator) => iterator.map((dataId) => ExternalDataItem.buildWithLoadedAttributes( this._dataClass, dataId, attributes ) ) ).iterator(); } private assertNoIdFilter() { const { filters } = this._params; if (filters && Object.keys(filters).includes('_id')) { throw new ArgumentError( `Cannot create a ${this.dataClassName()} from a scope that includes "_id" in its filters` ); } } private itemIdFromFilters() { return itemIdFromFilters(this._params.filters); } private hasSingleFilter() { const { filters, search } = this._params; return filters && Object.keys(filters).length === 1 && !search; } } /** @beta */ export class ExternalDataItem extends DataItem { /** Returns an item if its schema is loaded. Returns null otherwise. */ /** Triggers schema loading, thus requires a loading context. */ static build(dataClass: DataClass, dataId: string): ExternalDataItem | null { const attributes = getDataAttributeDefinitions(dataClass.name()); return attributes ? new ExternalDataItem(dataClass, dataId) : null; } /** Returns an item for an already loaded schema */ static buildWithLoadedAttributes( dataClass: DataClass, dataId: string, attributes: DataAttributeDefinitions ): ExternalDataItem { if (!attributes) throw new InternalError(); return new ExternalDataItem(dataClass, dataId); } /** Only for DataClass#getUnchecked */ static buildUnchecked( dataClass: DataClass, dataId: string ): ExternalDataItem { return new ExternalDataItem(dataClass, dataId); } private constructor( private readonly _dataClass: DataClass, private readonly _dataId: string ) { super(); } id(): string { return this._dataId; } dataClass(): DataClass { return this._dataClass; } dataClassName(): string { return this._dataClass.name(); } obj(): undefined { return; } get(attributeName: string): unknown { const externalData = this.getExternalData(); if (!externalData) return null; const dataClassName = this.dataClassName(); const attributes = getDataAttributeDefinitions(dataClassName); const { customData } = externalData; return attributes ? deserializeDataAttribute({ dataClassName, attributeName, value: customData[attributeName], attributes, }) : null; } /** @internal */ getRaw(attributeName: string): unknown { return this.getExternalData()?.customData[attributeName]; } /** @internal */ /** In contrast to `#get` supports both system and custom attributes */ getLocalized(attributeName: string): unknown { const attributeValue = this.getSystemOrCustom(attributeName); if (typeof attributeValue !== 'string') return attributeValue; const attributeDefinition = this.attributeDefinitions()[attributeName]; if (!attributeDefinition) return attributeValue; const [attributeType, attributeConfig] = attributeDefinition; if (attributeType === 'enum') { const valueConfig = attributeConfig.values.find( (config) => config.value === attributeValue ); if (!valueConfig) throw new InternalError(); const { title } = valueConfig; return typeof title === 'string' ? title : attributeValue; } return attributeValue; } async update(attributes: DataItemAttributes): Promise<void> { assertValidDataItemAttributes(attributes); const externalData = await load(() => this.getExternalData()); if (!externalData) { throw new ArgumentError(`Missing data with ID ${this._dataId}`); } const dataClassName = this.dataClassName(); const dataAttributeDefinitions = await loadAttributesOrThrow(dataClassName); const serializedAttributes = serializeAttributes( dataClassName, attributes, dataAttributeDefinitions ); const updatedData = await updateViaDataConnection( this.dataClassName(), this._dataId, serializedAttributes ); setExternalData(dataClassName, this._dataId, { systemData: externalData.systemData, customData: { ...externalData.customData, ...updatedData, }, }); this.notifyWrite(); } async delete(): Promise<void> { await deleteViaDataConnection(this.dataClassName(), this._dataId); setExternalData(this.dataClassName(), this._dataId, null); this.notifyWrite(); } /** @internal */ getCustomAttributes(): ExternalCustomAttributes { return this.getExternalData()?.customData ?? {}; } private getExternalData(): NormalExternalData | null | undefined { return getExternalData(this.dataClassName(), this._dataId); } private getSystemOrCustom(attributeName: string) { if (attributeName === '_id') return this.id(); return this.get(attributeName); } private notifyWrite() { notifyExternalDataWrite(this.dataClassName()); } } function serializeAttributes( dataClassName: string, attributes: DataItemAttributes, dataAttributeDefinitions: DataAttributeDefinitions ) { return mapValues(attributes, (value, attributeName) => serializeDataAttribute({ dataClassName, attributeName, value, attributes: dataAttributeDefinitions, }) ); } function isCommunicationError( error: unknown ): error is ClientError | DataConnectionError { return error instanceof ClientError || error instanceof DataConnectionError; } function handleCommunicationError<T>(request: () => T) { try { return request(); } catch (error) { if (isCommunicationError(error)) throw new DataScopeError(error.message); throw error; } } async function loadAttributesOrThrow(dataClassName: string) { const attributes = await load(() => getDataAttributeDefinitions(dataClassName) ); // A schema must be stored first if (!attributes) throw new InternalError(); return attributes; }