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.

474 lines (394 loc) 12 kB
import mapValues from 'lodash-es/mapValues'; import { isObject, logError } from 'scrivito_sdk/common'; import { LazyAsync, normalizeLazyAsync, } from 'scrivito_sdk/data_integration/lazy_async'; import { createLoadableCollection } from 'scrivito_sdk/loadable'; import { createStateContainer } from 'scrivito_sdk/state'; export type LazyAsyncDataClassSchema = LazyAsync<DataClassSchema>; type DataClassSchemaCallback = () => Promise<DataClassSchema>; export interface DataClassSchema { attributes?: LazyAsyncDataAttributeDefinitions; title?: LazyAsyncDataClassTitle; } export type LazyAsyncDataAttributeDefinitions = LazyAsync<DataAttributeDefinitions>; /** @public */ export interface DataAttributeDefinitions { [attributeName: string]: DataAttributeDefinition; } export type LazyAsyncDataClassTitle = LazyAsync<DataClassTitle>; type DataClassTitle = string | undefined; export interface DataClassSchemaResponse { attributes: DataAttributeDefinitions; title?: DataClassTitle; } export type DataAttributeDefinition = | DataAttributeDefinitionWithOptionalConfig | DataAttributeDefinitionWithConfig; export interface NormalizedDataAttributeDefinitions { [attributeName: string]: NormalizedDataAttributeDefinition; } export type NormalizedDataAttributeDefinition = | [DataAttributeDefinitionWithOptionalConfig, object] | NormalizedDataAttributeDefinitionWithConfig; export type NormalizedDataAttributeDefinitionWithConfig = { [T in keyof NormalizedDataAttributeConfigs]: [ T, NormalizedDataAttributeConfigs[T] ]; }[keyof NormalizedDataAttributeConfigs]; export interface NormalizedDataAttributeConfigs extends DataAttributeConfigs { enum: LocalizedEnumAttributeConfig; } export interface LocalizedEnumAttributeConfig extends EnumAttributeConfig { values: Array<LocalizedEnumValueConfig>; } export type DataAttributeConfigs = { boolean: LocalizedAttributeConfig; date: LocalizedAttributeConfig; enum: EnumAttributeConfig; number: LocalizedAttributeConfig; reference: ReferenceAttributeConfig; string: LocalizedAttributeConfig; unknown: UnknownAttributeConfig; }; type LocalizedAttributeConfig = { title?: string }; export interface EnumAttributeConfig extends LocalizedAttributeConfig { values: readonly EnumValueConfig[]; } export interface ReferenceAttributeConfig extends LocalizedAttributeConfig { to: DataClassName; reverseTitle?: string; } export interface UnknownAttributeConfig extends LocalizedAttributeConfig { type?: string; } type DataAttributeDefinitionWithOptionalConfig = Exclude< DataAttributeType, 'enum' | 'reference' >; export type DataAttributeDefinitionWithConfig = { [T in keyof DataAttributeConfigs]: readonly [T, DataAttributeConfigs[T]]; }[keyof DataAttributeConfigs]; export type DataAttributeConfig = DataAttributeDefinitionWithConfig[1]; export type DataAttributeType = | 'boolean' | 'date' | 'enum' | 'number' | 'reference' | 'string' | 'unknown'; type EnumValueConfig = string | LocalizedEnumValueConfig; type LocalizedEnumValueConfig = { value: string; title: string }; type DataClassName = string; function isDataAttributeType( attributeType: unknown ): attributeType is DataAttributeType { return ( typeof attributeType === 'string' && [ 'boolean', 'date', 'enum', 'number', 'reference', 'string', 'unknown', ].includes(attributeType) ); } export function registerDataClassSchema( dataClassName: string, schema: LazyAsyncDataClassSchema ): void { const schemata = { ...schemataState.get() }; schemata[dataClassName] = normalizeLazyAsync(schema); schemataState.set(schemata); invalidateSchemataCollection(); } export function getDataAttributeDefinitions( dataClassName: string ): DataAttributeDefinitions | undefined { return schemataCollection.get(dataClassName).get()?.attributes; } export function getDataClassTitle(dataClassName: string): DataClassTitle { return schemataCollection.get(dataClassName).get()?.title; } export function getNormalizedDataAttributeDefinitions( dataClassName: string ): NormalizedDataAttributeDefinitions { return mapValues( getDataAttributeDefinitions(dataClassName), normalizeDataAttributeDefinition ); } // For test purpose only export function unregisterDataClassSchema(dataClassName: string): void { const schemata = { ...schemataState.get() }; delete schemata[dataClassName]; schemataState.set(schemata); invalidateSchemataCollection(); } interface Schemata { [dataClassName: string]: DataClassSchemaCallback; } const schemataState = createStateContainer<Schemata>(); const counterState = createStateContainer<number>(); const schemataCollection = createLoadableCollection({ name: 'dataClassSchema', loadElement: (dataClassName: string) => ({ async loader() { const callback = schemataState.get()?.[dataClassName]; if (callback) { const data = await callback(); return { attributes: data.attributes instanceof Function ? await data.attributes() : await data.attributes, title: data.title instanceof Function ? await data.title() : await data.title, }; } return { attributes: {} }; }, invalidation: () => getCounter().toString(), }), }); function invalidateSchemataCollection() { counterState.set(getCounter() + 1); } function getCounter() { return counterState.get() || 0; } function normalizeDataAttributeDefinition( definition: DataAttributeDefinition ): NormalizedDataAttributeDefinition { if (typeof definition === 'string') return [definition, {}]; const [type, config] = definition; if (type === 'enum') return [type, normalizeEnumValueConfig(config)]; return [...definition]; } function normalizeEnumValueConfig({ title, values }: EnumAttributeConfig) { const config: LocalizedEnumAttributeConfig = { values: values.map((value) => typeof value === 'string' ? { value, title: value } : value ), }; if (title) config.title = title; return config; } export function extractDataClassSchemaResponse( input: unknown ): DataClassSchemaResponse { const response: DataClassSchemaResponse = { attributes: {}, title: undefined, }; if (!isObject(input)) { logError( `Invalid schema response: expected an object: ${JSON.stringify(input)}` ); return response; } if (!('attributes' in input)) { logError( `Invalid schema response: no "attributes" key: ${JSON.stringify(input)}` ); return response; } if ('title' in input && typeof input.title === 'string') { response.title = input.title; } response.attributes = extractDataAttributeDefinitions(input.attributes); return response; } function extractDataAttributeDefinitions( input: unknown ): DataAttributeDefinitions { const attributes: DataAttributeDefinitions = {}; if (!isObject(input)) { logError( `Invalid schema response: expected "attributes" to be an object: ${JSON.stringify( input )}` ); return attributes; } Object.entries(input).forEach(([attributeName, maybeDefinition]) => { if (attributeName === '_id') { logSchemaError( attributeName, maybeDefinition, 'Key "_id" is not allowed in schema attributes' ); } else { if (typeof maybeDefinition === 'string') { if (isDataAttributeDefinitionWithOptionalConfig(maybeDefinition)) { attributes[attributeName] = maybeDefinition; } else { logSchemaError( attributeName, maybeDefinition, 'Unknown attribute type.' ); } } else if (Array.isArray(maybeDefinition)) { const definition = extractDefinitionWithConfig( attributeName, maybeDefinition ); if (definition) { attributes[attributeName] = definition; } } else { logSchemaError( attributeName, maybeDefinition, 'Expected an array or a string' ); } } }); return attributes; } function isDataAttributeDefinitionWithOptionalConfig( definition: unknown ): definition is DataAttributeDefinitionWithOptionalConfig { return ( typeof definition === 'string' && ['boolean', 'date', 'number', 'string'].includes(definition) ); } function extractDefinitionWithConfig( attributeName: string, definition: unknown[] ): DataAttributeDefinitionWithConfig | undefined { if (definition.length < 2) { logSchemaError( attributeName, definition, 'Expected an array with two elements.' ); return; } const [attributeType, maybeConfig] = definition; if (!isDataAttributeType(attributeType)) { logSchemaError(attributeName, attributeType, 'Unknown attribute type.'); return; } switch (attributeType) { case 'enum': return extractEnumDefinitionWithConfig(attributeName, maybeConfig); case 'reference': return extractReferenceDefinitionWithConfig(attributeName, maybeConfig); default: return extractLocalizedAttributeConfig( attributeName, attributeType, maybeConfig ); } } function extractLocalizedAttributeConfig( attributeName: string, attributeType: Exclude<DataAttributeType, 'enum' | 'reference'>, maybeConfig: unknown ): DataAttributeDefinitionWithConfig | undefined { const config = isLocalizedAttributeConfig(maybeConfig) ? maybeConfig : undefined; if (config) return [attributeType, config]; logSchemaError(attributeName, maybeConfig, 'Invalid localization.'); } export function isEnumAttributeConfig( config: unknown ): config is EnumAttributeConfig { return ( isObject(config) && titleIsValidOrNotPresent(config) && 'values' in config && Array.isArray(config.values) && config.values.every( (valueOrConfig) => (typeof valueOrConfig === 'string' && valueOrConfig.length) || isLocalizedEnumValueConfig(valueOrConfig) ) ); } function extractEnumDefinitionWithConfig( attributeName: string, maybeConfig: unknown ): DataAttributeDefinitionWithConfig | undefined { if (isEnumAttributeConfig(maybeConfig)) { return ['enum', maybeConfig]; } logSchemaError( attributeName, maybeConfig, 'Invalid "enum" attribute config.' ); } function extractReferenceDefinitionWithConfig( attributeName: string, maybeConfig: unknown ): DataAttributeDefinitionWithConfig | undefined { const config = isReferenceAttributeConfig(maybeConfig) ? maybeConfig : undefined; if (config) return ['reference', config]; logSchemaError( attributeName, maybeConfig, 'Invalid "reference" attribute config.' ); } function isLocalizedEnumValueConfig( config: unknown ): config is LocalizedEnumValueConfig { return ( isObject(config) && 'value' in config && typeof config.value === 'string' && !!config.value.length && titleIsValidOrNotPresent(config) ); } function isReferenceAttributeConfig( config: unknown ): config is ReferenceAttributeConfig { return ( isObject(config) && 'to' in config && typeof config.to === 'string' && titleIsValidOrNotPresent(config) && (!('reverseTitle' in config) || typeof config.reverseTitle === 'string') ); } function isLocalizedAttributeConfig( config: unknown ): config is LocalizedAttributeConfig { return ( isObject(config) && (!Object.keys(config).length || titleIsValidOrNotPresent(config)) ); } function titleIsValidOrNotPresent(object: object) { return !('title' in object) || typeof object.title === 'string'; } function logSchemaError( attributeName: string, actual: unknown, details?: string ) { logError( `Invalid schema definition for attribute "${attributeName}": ${JSON.stringify( actual )}${details ? `\nDetails: ${details}` : ''}` ); }