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.

438 lines (361 loc) 10.7 kB
import isEqual from 'lodash-es/isEqual'; import mapValues from 'lodash-es/mapValues'; import { FilterValue } from 'scrivito_sdk/client'; import { ArgumentError, ScrivitoError, isObject } from 'scrivito_sdk/common'; import { NormalizedDataAttributeDefinition, NormalizedDataAttributeDefinitions, getDataClassTitle, getNormalizedDataAttributeDefinitions, } from 'scrivito_sdk/data_integration/data_class_schema'; import { isValidDataIdentifier } from 'scrivito_sdk/models'; import type { Obj, ObjSearch } from 'scrivito_sdk/realm'; /** @public */ export abstract class DataClass { /** @public */ abstract create(attributes: DataItemAttributes): Promise<DataItem>; /** @public */ abstract all(): DataScope; /** @public */ abstract get(id: string): DataItem | null; /** @beta */ abstract getUnchecked(id: string): DataItem; /** @public */ abstract name(): string; /** @public */ attributeDefinitions(): NormalizedDataAttributeDefinitions { return getNormalizedDataAttributeDefinitions(this.name()); } /** @internal */ title(): string | undefined { return getDataClassTitle(this.name()); } /** @internal */ forAttribute(attributeName: string): DataScope { return this.all().transform({ attributeName }); } } /** @public */ export abstract class DataScope { /** @public */ abstract dataClass(): DataClass | null; /** @public */ abstract dataClassName(): string | null; /** @beta */ abstract get(id: string): DataItem | null; /** @public */ abstract create(attributes: DataItemAttributes): Promise<DataItem>; /** @public */ abstract take(): DataItem[]; /** @public */ abstract transform(params: DataScopeParams): DataScope; /** @public */ abstract objSearch(): ObjSearch | undefined; /** @public */ abstract count(): number | null; /** @public */ abstract isDataItem(): boolean; /** @public */ abstract dataItem(): DataItem | null; /** @public */ abstract attributeName(): string | null; /** @public */ dataItemAttribute(): DataItemAttribute | null { const attributeName = this.attributeName(); if (!attributeName) return null; const dataItem = this.dataItem(); if (!dataItem) return null; return new DataItemAttribute(dataItem, attributeName); } /** @public */ isEmpty(): boolean { return this.transform({ limit: 1 }).take().length === 0; } /** @public */ containsData(): boolean { return !this.isEmpty(); } /** @public */ abstract limit(): number | undefined; /** @internal */ abstract toPojo(): DataScopePojo; /** @internal */ normalizeFilters( filters?: DataScopeFilters ): NormalizedDataScopeFilters | undefined { if (!filters) return; return mapValues(filters, (valueOrSpec, attributeName) => { if (isAndOperatorSpec(valueOrSpec)) return valueOrSpec; const isOpSpec = isOperatorSpec(valueOrSpec); const actualValue = isOpSpec ? valueOrSpec.value : valueOrSpec; let serializedValue = actualValue; if (actualValue instanceof Date) { serializedValue = actualValue.toISOString(); } if (actualValue instanceof DataItem) { serializedValue = actualValue.id(); } if ( serializedValue === null || typeof serializedValue === 'string' || typeof serializedValue === 'number' || typeof serializedValue === 'boolean' ) { const operator = isOpSpec ? valueOrSpec.operator : 'equals'; return { operator, value: serializedValue }; } throw new ArgumentError( `Invalid filter value for "${attributeName}": ${JSON.stringify( valueOrSpec )}` ); }); } protected attributesFromFilters(filters?: NormalizedDataScopeFilters) { if (!filters) return; const initialAttributes: Record<string, unknown> = {}; return Object.keys(filters).reduce((attributes, name) => { const filter = filters[name]; const specs = isOperatorSpec(filter) ? [filter] : filter.value; specs.forEach((spec) => { if (spec.operator === 'equals') attributes[name] = spec.value; }); return attributes; }, initialAttributes); } } export class DataItemAttribute { constructor( private readonly _dataItem: DataItem, private readonly _attributeName: string ) {} dataClass(): DataClass { return this._dataItem.dataClass(); } dataClassName(): string { return this._dataItem.dataClassName(); } dataItem(): DataItem { return this._dataItem; } attributeName(): string { return this._attributeName; } get(): unknown { return this._dataItem.get(this._attributeName); } async update(value: unknown): Promise<void> { return this._dataItem.update({ [this._attributeName]: value }); } /** @internal */ attributeDefinition(): NormalizedDataAttributeDefinition | null { return this.dataClass().attributeDefinitions()[this._attributeName] || null; } } /** @public */ export type DataItemAttributes = Record<string, unknown>; export type NormalizedDataScopeFilters = Record< string, OperatorSpec | AndOperatorSpec >; /** @public */ export type DataScopeFilters = Record< string, DataScopeFilterValue | DataScopeOperatorSpec | AndOperatorSpec >; type DataScopeFilterValue = FilterValue | Date | DataItem; export interface NormalizedDataScopeParams extends DataScopeParams { filters?: NormalizedDataScopeFilters; } /** @public */ export interface DataScopeParams { filters?: DataScopeFilters; search?: string; order?: OrderSpec; limit?: number; attributeName?: string; } export type FilterOperator = | 'equals' | 'notEquals' | 'isGreaterThan' | 'isLessThan' | 'isGreaterThanOrEquals' | 'isLessThanOrEquals'; export const DEFAULT_LIMIT = 20; export interface AndOperatorSpec { operator: 'and'; value: OperatorSpec[]; } /** @public */ export interface OperatorSpec extends DataScopeOperatorSpec { value: FilterValue; } interface DataScopeOperatorSpec { operator: FilterOperator; value: DataScopeFilterValue; } /** @public */ export type OrderSpec = Array<[string, 'asc' | 'desc']>; export type DataScopePojo = PresentDataScopePojo | EmptyDataScopePojo; export type PresentDataScopePojo = { _class: string; _attribute?: string; } & NormalizedDataScopeParams; export type EmptyDataScopePojo = { _class: null | string; _error?: string; isEmpty: true; isDataItem: boolean; }; export interface DataItemPojo { _id: string; _class: string; isBackground?: true; } /** @public */ export abstract class DataItem { /** @public */ abstract id(): string; /** @public */ abstract dataClass(): DataClass; /** @public */ abstract dataClassName(): string; /** @public */ abstract obj(): Obj | undefined; /** @public */ abstract get(attributeName: string): unknown; /** @public */ abstract update(attributes: DataItemAttributes): Promise<void>; /** @public */ abstract delete(): Promise<void>; /** @public */ attributeDefinitions(): NormalizedDataAttributeDefinitions { return this.dataClass().attributeDefinitions(); } /** @internal */ title(): string | undefined { return this.dataClass().title(); } /** @internal */ getRaw(_attributeName: string): unknown { return; } /** @internal */ getLocalized(attributeName: string): unknown { return this.get(attributeName); } } /** @public */ export class DataScopeError extends ScrivitoError { /** @internal */ constructor(readonly message: string) { super(message); } } export function assertValidDataItemAttributes( attributes: unknown ): asserts attributes is DataItemAttributes { if (!isObject(attributes)) { throw new ArgumentError('Data item attributes must be an object'); } if (!Object.keys(attributes as Object).every(isValidDataIdentifier)) { throw new ArgumentError( 'Keys of data item attributes must be valid data identifiers' ); } } export function combineFilters( currFilters: NormalizedDataScopeFilters | undefined, nextFilters: NormalizedDataScopeFilters | undefined ): NormalizedDataScopeFilters | undefined { if (!currFilters) return nextFilters; if (!nextFilters) return currFilters; let combinedFilters = { ...currFilters }; Object.keys(nextFilters).forEach((attributeName) => { if ( attributeName in combinedFilters && !isEqual(combinedFilters[attributeName], nextFilters[attributeName]) ) { const currentFilter = combinedFilters[attributeName]; const nextFilter = nextFilters[attributeName]; combinedFilters = { ...combinedFilters, [attributeName]: { operator: 'and', value: [ ...(isOperatorSpec(currentFilter) ? [currentFilter] : currentFilter.value), ...(isOperatorSpec(nextFilter) ? [nextFilter] : nextFilter.value), ], }, }; return; } combinedFilters[attributeName] = nextFilters[attributeName]; }); return combinedFilters; } export function combineSearches( currSearch: string | undefined, nextSearch: string | undefined ): string | undefined { return currSearch && nextSearch ? `${currSearch} ${nextSearch}` : currSearch || nextSearch; } export function itemPojoToScopePojo({ _class, _id, }: DataItemPojo): DataScopePojo { return { _class, filters: { _id: { operator: 'equals', value: _id } } }; } export function scopePojoToItemPojo({ _class, filters, }: PresentDataScopePojo): DataItemPojo | undefined { const id = itemIdFromFilters(filters); if (id) return { _class, _id: id }; } export function itemIdFromFilters( filters: NormalizedDataScopeFilters | undefined ): string | undefined { const id = filters?._id?.value; if (typeof id === 'string') return id; } export function isFilterOperator( operator: unknown ): operator is FilterOperator { return ( typeof operator === 'string' && [ 'equals', 'notEquals', 'isGreaterThan', 'isLessThan', 'isGreaterThanOrEquals', 'isLessThanOrEquals', ].includes(operator) ); } export function isOperatorSpec(spec: unknown): spec is OperatorSpec { return ( isObject(spec) && 'operator' in spec && isFilterOperator(spec.operator) && 'value' in spec && (spec.value === null || ['string', 'number', 'boolean'].includes(typeof spec.value)) ); } function isAndOperatorSpec(spec: unknown): spec is AndOperatorSpec { return ( isObject(spec) && 'operator' in spec && spec.operator === 'and' && 'value' in spec && Array.isArray(spec.value) && spec.value.every(isOperatorSpec) ); }