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.

606 lines (484 loc) 14.4 kB
import { EMPTY_SPACE, isEmptySpaceId } from 'scrivito_sdk/client'; import { ArgumentError, isSystemAttribute } from 'scrivito_sdk/common'; import { AndOperatorSpec, DEFAULT_LIMIT, DataClass, DataItem, DataItemAttributes, DataScope, DataScopeParams, NormalizedDataScopeParams, OperatorSpec, PresentDataScopePojo, combineFilters, combineSearches, itemIdFromFilters, } from 'scrivito_sdk/data_integration/data_class'; import { NormalizedDataAttributeDefinition, NormalizedDataAttributeDefinitions, } from 'scrivito_sdk/data_integration/data_class_schema'; import { getObjDataClass } from 'scrivito_sdk/data_integration/get_data_class'; import { BasicObj, BasicObjAttributes, BasicObjSearch, createObjIn, currentObjSpaceId, excludeDeletedObjs, getObjFrom, objSpaceScope, restrictToObjClass, } from 'scrivito_sdk/models'; import { NormalizedAttributeDefinition, Obj, ObjSearch, Schema, getRealmClass, unwrapAppClass, wrapInAppClass, } from 'scrivito_sdk/realm'; const TYPES_WITH_SCHEMA_SUPPORT = [ 'boolean', 'enum', 'float', 'integer', 'string', 'reference', ]; const TYPES_WITH_GARBAGE_IN_GARBAGE_OUT_SUPPORT = ['multienum', 'stringlist']; const SUPPORTED_ATTRIBUTE_TYPES = [ ...TYPES_WITH_SCHEMA_SUPPORT, ...TYPES_WITH_GARBAGE_IN_GARBAGE_OUT_SUPPORT, ]; // Exported for test purpose only export const SUBPAGES_CHILD_ORDER_LIMIT = 200; export class ObjDataClass 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 ObjDataScope(this); } get(id: string): DataItem | null { const obj = getDataObj(this, id); return obj ? new ObjDataItem(this, id) : null; } getUnchecked(id: string): DataItem { return new ObjDataItem(this, id); } attributeDefinitions(): NormalizedDataAttributeDefinitions { return attributeDefinitions(this._name); } } function getDataObj(dataClass: DataClass, dataId: string): BasicObj | null { return getObjFrom(objClassScope(dataClass).and(excludeDeletedObjs), dataId); } export class ObjDataScope 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> { if (this.isBuiltInClass()) { throw new ArgumentError( 'Cannot create data items using the built-in Obj class' ); } const obj = createObjIn( this.objClassScope(), prepareAttributes( { ...attributes, ...this.attributesFromFilters(this._params.filters) }, this._dataClass.name() ) ); // Important: Wait for saving to finish await obj.finishSaving(); return this.wrapInDataItem(obj); } get(id: string): DataItem | null { const [obj] = this.getSearch().and('_id', 'equals', id).take(1); if (!obj) return null; return this.wrapInDataItem(obj); } take(): DataItem[] { const search = this.getSearch(); const parentObj = this.parentObj(); const limit = this._params.limit ?? DEFAULT_LIMIT; let objs: BasicObj[]; if ( !this._params.search && !this._params.order && parentObj?.hasChildOrder() ) { objs = parentObj .sortByChildOrder( search.take(Math.max(limit, SUBPAGES_CHILD_ORDER_LIMIT)) ) .slice(0, limit); } else { objs = search.take(limit); } return objs.map((obj) => this.wrapInDataItem(obj)); } dataItem(): DataItem | null { const id = this.itemIdFromFilters(); if (id) return this._dataClass.get(id); return null; } isDataItem(): boolean { return !!this.itemIdFromFilters(); } attributeName(): string | null { return this._attributeName || null; } count(): number { return this.getSearch().count(); } transform({ filters, search, order, limit, attributeName, }: DataScopeParams): DataScope { return new ObjDataScope( 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, } ); } limit(): number | undefined { return this._params.limit; } objSearch(): ObjSearch | undefined { const search = this.getSearch(); if (!isEmptySpaceId(search.objSpaceId())) { return new ObjSearch(search); } } /** @internal */ toPojo(): PresentDataScopePojo { return { _class: this.dataClassName(), _attribute: this._attributeName, ...this._params, }; } private getSearch() { let initialSearch = this.getInitialSearch(); const { filters, search: searchTerm, order: givenOrder } = this._params; if (searchTerm) { initialSearch = initialSearch.and('*', 'matches', searchTerm); } if (givenOrder) { const order = givenOrder.filter(([attributeName]) => !!attributeName); if (order.length) initialSearch = initialSearch.order(order); } if (!filters) return initialSearch; return Object.keys(filters) .filter((name) => !!name) .reduce( (search, name) => this.applyFilter(search, name, filters[name]), initialSearch ); } private getInitialSearch() { return (this.isBuiltInClass() ? currentObjScope() : this.objClassScope()) .and(excludeDeletedObjs) .search(); } private isBuiltInClass() { return isBuiltInClass(this.dataClassName()); } private applyFilter( search: BasicObjSearch, attributeName: string, operatorSpec: OperatorSpec | AndOperatorSpec ): BasicObjSearch { const { operator, value } = operatorSpec; if (operator === 'and') { return value.reduce( (currentSearch, spec) => this.applyFilter(currentSearch, attributeName, spec), search ); } if (operator === 'equals') { if (attributeName === '_obj_parent_id') { return this.applySubpagesFilter(search); } return search.and(attributeName, 'equals', value); } if (operator === 'notEquals') { return search.andNot(attributeName, 'equals', value); } if (operator === 'isGreaterThan') { return search.and(attributeName, 'isGreaterThan', value); } if (operator === 'isLessThan') { return search.and(attributeName, 'isLessThan', value); } if (operator === 'isGreaterThanOrEquals') { return search.andNot(attributeName, 'isLessThan', value); } if (operator === 'isLessThanOrEquals') { return search.andNot(attributeName, 'isGreaterThan', value); } throw new ArgumentError(`Unknown filter operator "${operator}"`); } private applySubpagesFilter(search: BasicObjSearch) { const parentObj = this.parentObj(); const siteId = parentObj?.siteId(); const parentPath = parentObj?.path(); if (!siteId || !parentPath) { return objSpaceScope(EMPTY_SPACE).search(); } return search .and('_siteId', 'equals', siteId) .and('_parentPath', 'equals', parentPath); } private objClassScope() { return objClassScope(this._dataClass); } private wrapInDataItem(obj: BasicObj) { const item = new ObjDataItem(this._dataClass, obj.id()); item.setBasicObj(obj); return item; } private itemIdFromFilters() { return itemIdFromFilters(this._params.filters); } private parentObj() { const parentId = this._params.filters?._obj_parent_id?.value; if (typeof parentId === 'string') { return getObjFrom(currentObjScope().and(excludeDeletedObjs), parentId); } } } export class ObjDataItem extends DataItem { private _obj: BasicObj | null | undefined; 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(): Obj { return wrapInAppClass(this.getOrThrow()); } /** @internal */ getBasicObj(): BasicObj | null { if (this._obj === undefined) { this._obj = getDataObj(this._dataClass, this._dataId); } return this._obj; } /** @internal */ setBasicObj(obj: BasicObj): void { this._obj = obj; } get(attributeName: string): unknown { const obj = this.getBasicObj(); if (!obj) return null; const typeInfo = getAttributeTypeInfo(obj.objClass(), attributeName); if (!typeInfo) return null; const [attributeType, attributeConfig] = typeInfo; if (SUPPORTED_ATTRIBUTE_TYPES.includes(attributeType)) { return attributeType === 'reference' ? getReference(obj, attributeName, attributeConfig) : obj.get(attributeName, typeInfo); } return null; } update(attributes: DataItemAttributes): Promise<void> { const obj = this.getOrThrow(); obj.update(prepareAttributes(attributes, this.dataClassName())); return obj.finishSaving(); } async delete(): Promise<void> { const obj = this.getBasicObj(); if (obj) { obj.delete(); return obj.finishSaving(); } } private getOrThrow() { const obj = this.getBasicObj(); if (!obj) { throw new ArgumentError(`Missing obj with ID ${this._dataId}`); } return obj; } } function getAttributeTypeInfo(className: string, attributeName: string) { return getSchema(className).attribute(attributeName); } export function isObjDataClassProvided(className: string): boolean { return !!getRealmClass(className); } function getSchema(className: string) { const objClass = getRealmClass(className); if (!objClass) { throw new ArgumentError(`Class ${className} does not exist`); } const schema = Schema.forClass(objClass); if (!schema) { throw new ArgumentError(`Class ${className} has no schema`); } return schema; } function objClassScope(dataClass: DataClass) { const dataScope = currentObjScope(); const dataClassName = dataClass.name(); return dataClassName === 'Obj' ? dataScope : dataScope.and(restrictToObjClass(dataClassName)); } function prepareAttributes(attributes: DataItemAttributes, className: string) { const preparedAttributes: BasicObjAttributes = {}; Object.keys(attributes).forEach((attributeName) => { const attributeValue = attributes[attributeName]; if (isSystemAttribute(attributeName)) { preparedAttributes[attributeName] = attributeValue; } else { const typeInfo = getAttributeTypeInfo(className, attributeName); if (!typeInfo) { throw new ArgumentError( `Attribute ${attributeName} of class ${className} does not exist` ); } const [attributeType, attributeConfig] = typeInfo; if (!SUPPORTED_ATTRIBUTE_TYPES.includes(attributeType)) { throw new ArgumentError( `Attribute ${attributeName} of class ${className} has unsupported type ${attributeType}` ); } preparedAttributes[attributeName] = [ attributeType === 'reference' ? prepareReferenceValue(attributeValue, attributeConfig) : attributeValue, typeInfo, ]; } }); return preparedAttributes; } function getReference( obj: BasicObj, attributeName: string, attributeConfig?: { validClasses: readonly string[] } ) { if (!attributeConfig) return null; const referenceObj = obj.get(attributeName, 'reference'); if (!(referenceObj instanceof BasicObj)) return null; const referenceObjClass = referenceObj.objClass(); if (referenceObjClass !== getValidReferenceClass(attributeConfig)) { return null; } const dataClass = getObjDataClass(referenceObjClass); if (!dataClass) return null; return dataClass.get(referenceObj.id()); } function prepareReferenceValue( attributeValue: unknown, attributeConfig?: { validClasses: readonly string[] } ) { return attributeValue instanceof DataItem && attributeValue.dataClassName() === getValidReferenceClass(attributeConfig) ? unwrapAppClass(attributeValue.obj()) : null; } function getValidReferenceClass(attributeConfig?: { validClasses: readonly string[]; }) { if (attributeConfig) { const { validClasses } = attributeConfig; if (validClasses.length === 1) return validClasses[0]; } } function attributeDefinitions(dataClassName: string) { if (isBuiltInClass(dataClassName)) return {}; const attributes: NormalizedDataAttributeDefinitions = {}; const normalizedAttributes = getSchema(dataClassName).normalizedAttributes(); Object.keys(normalizedAttributes).forEach((attributeName) => { const dataAttributeDefinition = toDataAttributeDefinition( normalizedAttributes[attributeName] ); if (dataAttributeDefinition) { attributes[attributeName] = dataAttributeDefinition; } }); return attributes; } function toDataAttributeDefinition([ cmsType, cmsTypeInfo, ]: NormalizedAttributeDefinition): NormalizedDataAttributeDefinition | null { if (cmsType === 'boolean' || cmsType === 'string') { return [cmsType, {}]; } if (cmsType === 'float' || cmsType === 'integer') { return ['number', {}]; } if (cmsType === 'enum') { return [ 'enum', { values: [ ...cmsTypeInfo.values.map((value) => ({ value, title: value })), ], }, ]; } if (cmsType === 'reference') { const { only } = cmsTypeInfo; if (typeof only === 'string') { return ['reference', { to: only }]; } if (Array.isArray(only) && only.length === 1) { return ['reference', { to: only[0] }]; } } return null; } function currentObjScope() { return objSpaceScope(currentObjSpaceId()); } function isBuiltInClass(dataClassName: string) { return dataClassName === 'Obj'; }