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.

745 lines (599 loc) 19.2 kB
import { slugify } from '@justrelate/slugify'; import { AttributeJson, ExistentObjJson, ObjJson, ObjSpaceId, WidgetJson, WidgetPoolJson, isWidgetlistAttributeJson, } from 'scrivito_sdk/client'; import { InternalError, ScrivitoError, camelCase, computeAncestorPaths, equals, isSystemAttribute, parseStringToDate, randomHex, randomId, } from 'scrivito_sdk/common'; import { ObjData, WidgetPlacement, findWidgetPlacement, isUsingInMemoryTenant, } from 'scrivito_sdk/data'; import { finishLinkResolutionFor, startLinkResolutionFor, } from 'scrivito_sdk/link_resolution'; import * as AttributeSerializer from 'scrivito_sdk/models/attribute_serializer'; import { ContentValueOrConnection, ContentValueProvider, NormalizedBasicAttributesWithUnknownValues, getContentValue, getContentValueOrConnection, normalizeAttributes, normalizedRestriction, persistWidgets, serializeAttributes, } from 'scrivito_sdk/models/basic_attribute_content'; import { BasicAttributeValue, CmsAttributeType, } from 'scrivito_sdk/models/basic_attribute_types'; import { BasicField } from 'scrivito_sdk/models/basic_field'; import { BasicObjSearch, BasicSearchValue, FieldBoost, SearchField, SearchOperator, } from 'scrivito_sdk/models/basic_obj_search'; import { createObjIn } from 'scrivito_sdk/models/basic_scope_create_methods'; import { getAllObjsByValueFrom, getObjBy, getObjFrom, } from 'scrivito_sdk/models/basic_scope_get_methods'; import { BasicWidget } from 'scrivito_sdk/models/basic_widget'; import { Binary } from 'scrivito_sdk/models/binary'; import { computeParentPath } from 'scrivito_sdk/models/compute_parent_path'; import { currentObjSpaceId, isCurrentWorkspacePublished, } from 'scrivito_sdk/models/current_workspace_id'; import { MetadataCollection } from 'scrivito_sdk/models/metadata_collection'; import { objSpaceScope } from 'scrivito_sdk/models/obj_scope'; import { objSpaceScopeExcludingDeleted } from 'scrivito_sdk/models/obj_space_scope_excluding_deleted'; import { restrictToSite } from 'scrivito_sdk/models/restrict_to_site'; import { TypeInfo } from 'scrivito_sdk/models/type_info'; import { withBatchedUpdates } from 'scrivito_sdk/state'; interface WidgetInsertionBefore { before: BasicWidget; } type WidgetInsertionAnchor = | WidgetInsertionBefore | { after: BasicWidget; before?: undefined }; interface WidgetPlacementWithContainer extends WidgetPlacement { container: BasicObj | BasicWidget; attributeValue: | BasicAttributeValue<'widget'> | BasicAttributeValue<'widgetlist'>; } export interface BasicObjAttributes { [key: string]: unknown; _id?: string | [string]; _objClass?: string | [string]; _path?: string | [string] | null; } export interface SerializedObjAttributes { [key: string]: unknown; _id: string; _obj_class: string; _restriction?: [string] | null; _language?: string | null; _data_param?: [string] | null; } export class BasicObjSnapshot { constructor(readonly _data: ObjJson) {} } export class BasicObj implements ContentValueProvider { static get(id: string): BasicObj | null { return getObjFrom(currentObjSpaceWithoutDeleted(), id); } static getIncludingDeleted(id: string): BasicObj | null { return getObjFrom(objSpaceScope(currentObjSpaceId()), id); } // Accessible for test purposes only (otherwise better inlined) static createInObjSpace( objSpaceId: ObjSpaceId, attributes: BasicObjAttributes ): BasicObj { return createObjIn(objSpaceScope(objSpaceId), attributes); } static generateId(): string { return randomId(); } static all(): BasicObjSearch { return new BasicObjSearch(currentObjSpaceId()).batchSize(1000); } static where( fields: SearchField, operator: SearchOperator, value: BasicSearchValue, boost?: FieldBoost ): BasicObjSearch { return new BasicObjSearch(currentObjSpaceId()).and( fields, operator, value, boost ); } static getByPermalink(permalink: string): BasicObj | null { return getObjBy(currentObjSpaceWithoutDeleted(), '_permalink', permalink); } static getAllByPermalink(permalink: string): BasicObj[] { return getAllObjsByValueFrom( currentObjSpaceWithoutDeleted(), '_permalink', permalink ); } // For test purpose only. static generateWidgetId(): string { return randomHex(); } readonly objData: ObjData; constructor(objData: ObjData) { this.objData = objData; } id(): string { return this.objData.id(); } objClass(): string { return this.getAttributeData('_obj_class'); } obj(): this { return this; } createdAt(): Date | null { return parseStringToDate(this.getAttributeData('_created_at')); } createdBy(): string | null { return this.getAttributeData('_created_by') || null; } lastChanged(): Date | null { const data = this.getAttributeData('_last_changed'); if (!data) return null; return parseStringToDate(data); } lastChangedBy(): string | null { return this.getAttributeData('_last_changed_by') || null; } firstPublishedAt(): Date | null { return parseStringToDate(this.getAttributeData('_first_published_at')); } publishedAt(): Date | null { return parseStringToDate(this.getAttributeData('_published_at')); } firstPublishedBy(): string | null { return this.getAttributeData('_first_published_by') || null; } publishedBy(): string | null { return this.getAttributeData('_published_by') || null; } objSpaceId(): ObjSpaceId { return this.objData.objSpaceId(); } version(): string | undefined { return this.getAttributeData('_version'); } path(): string | null { return this.getAttributeData('_path') || null; } permalink(): string | null { return this.getAttributeData('_permalink') || null; } siteId(): string | null { return this.getAttributeData('_site_id') ?? null; } language(): string | null { return this.getAttributeData('_language') ?? null; } parentPath(): string | null { return computeParentPath(this.path()); } parent(): BasicObj | null { const parentPath = this.parentPath(); const siteId = this.siteId(); if (parentPath === null || siteId === null) return null; return getObjByPath(this.objSpaceId(), siteId, parentPath); } hasConflicts(): boolean { return !!this.getAttributeData('_conflicts'); } modification(): 'new' | 'edited' | 'deleted' | null { if ( this.objData.isUnavailable() || this.getAttributeData('_marked_deleted') ) { return 'deleted'; } return this.getAttributeData('_modification') || null; } dataClass(): string | null { return this.get('dataClass', 'string') || null; } dataParam(): [string] | null { return this.getAttributeData('_data_param') ?? null; } get<Type extends CmsAttributeType>( attributeName: string, typeInfo: TypeInfo<Type> ): BasicAttributeValue<Type> { return getContentValue(this, attributeName, typeInfo); } getValueOrConnection<Type extends CmsAttributeType>( attributeName: string, typeInfo: TypeInfo<Type> ): ContentValueOrConnection<Type> { return getContentValueOrConnection(this, attributeName, typeInfo); } isModified(): boolean { return !!this.modification(); } isNew(): boolean { return this.modification() === 'new'; } isEdited(): boolean { return this.modification() === 'edited'; } isEditingAsset(): boolean { return this.getAttributeData('_editing_asset') === true; } isDeleted(): boolean { return this.modification() === 'deleted'; } contentLength(): number { return this.metadata().contentLength(); } contentType(): string { return this.metadata().contentType(); } contentUrl(): string { return this.blob()?.url() || ''; } contentId(): string { return this.getAttributeData('_content_id') || this.id(); } metadata(): MetadataCollection { const blob = this.blob(); return blob ? new MetadataCollection(blob.id(), this.objSpaceId()) : new MetadataCollection(); } children(): BasicObj[] { const search = this.getChildrenSearch(); return search ? search.dangerouslyUnboundedTake() : []; } hasChildren(): boolean { const search = this.getChildrenSearch(); return search ? search.batchSize(0).count() > 0 : false; } orderedChildren(): BasicObj[] { return this.sortByChildOrder(this.children()); } sortByChildOrder(objs: BasicObj[]): BasicObj[] { if (objs.length === 0) return []; const idsOrder = this.childOrder().map((reference) => reference.id()); return objs .map((child: BasicObj): [number, BasicObj] => { const index = idsOrder.indexOf(child.id()); return [index === -1 ? objs.length : index, child]; }) .sort(([a], [b]) => a - b) .map(([, child]) => child); } hasChildOrder(): boolean { return this.childOrder().length > 0; } backlinks(): BasicObj[] { return objSpaceScopeExcludingDeleted(this.objSpaceId()) .search() .and('*', 'linksTo', this) .dangerouslyUnboundedTake(); } ancestors(): Array<BasicObj | null> { const parentPath = this.parentPath(); const siteId = this.siteId(); if (parentPath === null || siteId === null) return []; return computeAncestorPaths(parentPath).map((ancestorPath) => getObjByPath(this.objSpaceId(), siteId, ancestorPath) ); } restriction(): string { const restrictionAttribute = this.getAttributeData('_restriction'); return normalizedRestriction(restrictionAttribute); } restrict(restriction: string = '_auth'): void { this.update({ _restriction: restriction === '_public' ? null : [[restriction]], }); } unrestrict(): void { this.restrict('_public'); } isRestricted(): boolean { return this.restriction() !== '_public'; } update(attributes: BasicObjAttributes): void { const normalizedAttributes = normalizeAttributes(attributes); this.updateWithUnknownValues(normalizedAttributes); } updateWithUnknownValues( attributes: NormalizedBasicAttributesWithUnknownValues ): void { if (isCurrentWorkspacePublished() && !isUsingInMemoryTenant()) { throw new ScrivitoError('The published content cannot be modified.'); } withBatchedUpdates(() => { persistWidgets(this, attributes); const patch = AttributeSerializer.serialize(attributes); this.objData.update(patch); }); this.startLinkResolution(); } delete(): void { this.update({ _markedDeleted: [true] }); } insertWidget(widget: BasicWidget, anchor: WidgetInsertionAnchor): void { const id = widgetIdFromWidgetInsertionAnchor(anchor); const placement = this.widgetPlacementFor(id); if (placement) { const { attributeValue, attributeName, container, index } = placement; if (!Array.isArray(attributeValue)) throw new InternalError(); const newIndex = anchor.before ? index : index + 1; const newAttributeValue = [ ...attributeValue.slice(0, newIndex), widget, ...attributeValue.slice(newIndex), ]; container.update({ [attributeName]: [newAttributeValue, ['widgetlist']], }); } } deleteWidget(widget: BasicWidget): void { const widgetOrWidgetlistField = this.fieldContainingWidget(widget); if (!widgetOrWidgetlistField) return; if (widgetOrWidgetlistField.type() === 'widgetlist') { const widgetlistField = widgetOrWidgetlistField as BasicField<'widgetlist'>; const value = widgetlistField.get(); const newValue = value.filter((curWidget) => !curWidget.equals(widget)); widgetlistField.update(newValue); } else { const widgetField = widgetOrWidgetlistField as BasicField<'widget'>; widgetField.update(null); } } siblingWidget( widget: BasicWidget, indexOffset: -1 | 1 ): BasicWidget | undefined { const placement = this.widgetPlacementFor(widget.id()); if (placement) { const { attributeValue, index } = placement; if (Array.isArray(attributeValue)) { return attributeValue[index + indexOffset]; } } } markResolvedAsync(): Promise<void> { this.update({ _conflicts: [null] }); return this.finishSaving(); } async finishSaving(): Promise<void> { await this.finishLinkResolution(); return this.objData.finishSaving(); } equals(other: unknown): boolean { return ( other instanceof BasicObj && this.id() === other.id() && equals(this.objSpaceId(), other.objSpaceId()) ); } widget(id: string): BasicWidget | null { if (!this.getWidgetAttribute(id, '_obj_class')) return null; return BasicWidget.build(id, this); } getWidgetAttribute<Key extends keyof WidgetJson & string>( id: string, attributeName: Key ): WidgetJson[Key] { return this.objData.getWidgetAttribute(id, attributeName); } widgets(): BasicWidget[] { const data = this.objData.getIfExistent(); if (!data) return []; const widgetPool = data._widget_pool; if (!widgetPool) return []; const widgets: BasicWidget[] = []; const visitedWidgetIds: { [key: string]: true | undefined } = {}; this.collectWidgets(widgets, data, widgetPool, visitedWidgetIds); return widgets; } widgetClassNamesWithBadPerformance(): string[] { const widgetPool = this.objData.getWidgetPoolWithBadPerformance(); if (!widgetPool) return []; const classNames = new Set( Object.values(widgetPool) .filter((value): value is WidgetJson => !!value) .map((widgetJson) => widgetJson._obj_class) ); return Array.from(classNames); } fieldContainingWidget( widget: BasicWidget ): BasicField<'widget'> | BasicField<'widgetlist'> | undefined { const widgetId = widget.id(); const placement = this.widgetPlacementFor(widgetId); if (placement) { const { container, attributeName, attributeValue } = placement; return Array.isArray(attributeValue) ? new BasicField<'widgetlist'>(container, attributeName, ['widgetlist']) : new BasicField<'widget'>(container, attributeName, ['widget']); } } generateWidgetId(): string { for (let i = 0; i < 10; i++) { const id = BasicObj.generateWidgetId(); if (!this.widget(id)) return id; } // Could not generate a new unused widget id. // (winning the lottery 5 times in a row is more likely) throw new InternalError(); } serializeAttributes(): SerializedObjAttributes { const { _conflicts, _modification, _created_at, _created_by, _last_changed, _last_changed_by, ...serializedAttributes } = serializeAttributes(this); return serializedAttributes; } slug(): string { const title = this.get('title', 'string'); return slugify(title); } getWidgetData(id: string): WidgetJson | undefined { return this.objData.getWidget(id); } startLinkResolution(): void { if (!isUsingInMemoryTenant()) { startLinkResolutionFor(currentObjSpaceId(), this.id()); } } finishLinkResolution(): Promise<void> { return finishLinkResolutionFor(currentObjSpaceId(), this.id()); } toPrettyPrint(): string { return `[object ${this.objClass()} id="${this.id()}"]`; } getAttributeData<Key extends keyof ExistentObjJson & string>( attributeName: Key, type?: CmsAttributeType ): ExistentObjJson[Key] { return type === 'widget' || type === 'widgetlist' ? this.objData.getAttributeWithWidgetData(attributeName) : this.objData.getAttributeWithoutWidgetData(attributeName); } getData(): ObjJson | undefined { return this.objData.get(); } createSnapshot(): BasicObjSnapshot { return new BasicObjSnapshot(this.objData.getOrThrow()); } revertTo(snapshot: BasicObjSnapshot): void { this.objData.set(snapshot._data); } private blob(): Binary | null { return this.get('blob', ['binary']); } private collectWidgets( memo: BasicWidget[], objOrWidgetData: ExistentObjJson | WidgetJson, widgetPool: WidgetPoolJson, visitedWidgetIds: { [key: string]: true | undefined } ): void { Object.keys(objOrWidgetData) .map((attributeName) => { const attrDictValue = objOrWidgetData[attributeName]; if (!attrDictValue) return; if (isSystemAttribute(attributeName)) return; // Typescript cannot know that once blank and system attribute entries // are excluded, what's left must be a custom attribute entry, and the // cast is therefore safe. const attributeJson = attrDictValue as AttributeJson; if (isWidgetlistAttributeJson(attributeJson)) return attributeJson[1]; }) .forEach((widgetIds) => { if (widgetIds) { widgetIds.forEach((widgetId) => { if (visitedWidgetIds[widgetId]) return; visitedWidgetIds[widgetId] = true; const widget = this.widget(widgetId); if (!widget) return; memo.push(widget); const widgetData = widgetPool[widgetId]!; this.collectWidgets(memo, widgetData, widgetPool, visitedWidgetIds); }); } }); } private widgetPlacementFor( widgetId: string ): WidgetPlacementWithContainer | undefined { const data = this.objData.getIfExistent(); if (!data) return; const placement = findWidgetPlacement(data, widgetId); if (!placement) return; const attributeName = camelCase(placement.attributeName); const { attributeType, index, parentWidgetId } = placement; let container; if (parentWidgetId) { container = this.widget(parentWidgetId); if (!container) return; } else { container = this; } return { container, attributeName, attributeType, attributeValue: container.get(attributeName, [attributeType]), index, parentWidgetId, }; } private childOrder() { return this.get('childOrder', 'referencelist'); } private getChildrenSearch() { const path = this.path(); const siteId = this.siteId(); if (!path || siteId === null) return; return objSpaceScopeExcludingDeleted(this.objSpaceId()) .search() .andIsChildOf(this); } } function widgetIdFromWidgetInsertionAnchor(anchor: WidgetInsertionAnchor) { if (isWidgetInsertionBefore(anchor)) return anchor.before.id(); return anchor.after.id(); } function isWidgetInsertionBefore( anchor: WidgetInsertionAnchor ): anchor is WidgetInsertionBefore { return !!(anchor as WidgetInsertionBefore).before; } function currentObjSpaceWithoutDeleted() { return objSpaceScopeExcludingDeleted(currentObjSpaceId()); } function getObjByPath(objSpaceId: ObjSpaceId, siteId: string, path: string) { return getObjBy( objSpaceScopeExcludingDeleted(objSpaceId).and(restrictToSite(siteId)), '_path', path ); }