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.

372 lines (299 loc) 9.43 kB
import { ClientError } from 'scrivito_sdk/client'; import { ArgumentError, isObject, isPresent, isValidInteger, } from 'scrivito_sdk/common'; import { anticipatedDataConnection } from 'scrivito_sdk/data_integration/anticipated_data_connection'; import { DataConnectionError } from 'scrivito_sdk/data_integration/data_connection_error'; import { DataId, isValidDataId } from 'scrivito_sdk/data_integration/data_id'; import { ExternalData } from 'scrivito_sdk/data_integration/external_data'; import { DataConnectionIndexParams } from 'scrivito_sdk/data_integration/index_params'; import { LazyAsync } from 'scrivito_sdk/data_integration/lazy_async'; import { isValidDataIdentifier } from 'scrivito_sdk/models'; import { createStateContainer } from 'scrivito_sdk/state'; /** @public */ export interface DataConnection { index: IndexCallback; get: GetCallback; create: CreateCallback; update: UpdateCallback; delete: DeleteCallback; } /** @internal */ export interface UncheckedDataConnection { index: UncheckedIndexCallback; create: UncheckedCreateCallback; get: GetCallback; update: UpdateCallback; delete: DeleteCallback; } /** @public */ export type IndexCallback = ( params: DataConnectionIndexParams ) => Promise<IndexResult | DataConnectionError>; /** @internal */ type UncheckedIndexCallback = ( params: DataConnectionIndexParams ) => Promise<unknown>; /** @public */ export type GetCallback = (id: string) => Promise<unknown | null>; /** @public */ export type CreateCallback = ( data: ExternalData ) => Promise<DataConnectionResultItem>; type UncheckedCreateCallback = (data: ExternalData) => Promise<unknown>; /** @public */ export type UpdateCallback = ( id: string, data: ExternalData ) => Promise<unknown>; /** @public */ export type DeleteCallback = (id: string) => Promise<unknown>; /** @public */ export interface IndexResult { results: Array<DataId | number | DataConnectionResultItem>; continuation?: string | null; count?: IndexResultCount; } export interface NormalIndexResult { results: Array<DataId | number | NormalExternalData>; continuation?: string | null; count?: number; } export interface NormalExternalData { systemData: ExternalSystemAttributes; customData: ExternalCustomAttributes; } export interface ExternalSystemAttributes { _id: string; } export type ExternalCustomAttributes = Record<string, unknown>; type IndexResultCount = number | string | null; /** @public */ export type DataConnectionResultItem = | ResultItemNumericConvenienceId | ResultItemNumericId | ResultItemConvenienceId | ResultItemStringId; interface ResultItemNumericConvenienceId extends ResultItemData { _id?: undefined; id: number; } interface ResultItemNumericId extends ResultItemData { _id: number; } interface ResultItemConvenienceId extends ResultItemData { _id?: undefined; id: DataId; } export interface ResultItemStringId extends ResultItemData { _id: DataId; } export interface ResultItemData { [key: string]: unknown; } function assertValidNumericId(id: number) { if (id < 0 || !Number.isSafeInteger(id)) { throw new ArgumentError( `Numeric IDs must be a non-negative "safe" integer: ${id.toString()}` ); } } function parseResultItem(resultItem: unknown): NormalExternalData { if (!isObject(resultItem)) { throw new ArgumentError('A result item must be an object'); } if ('_id' in resultItem) { const { _id, ...customData } = resultItem; return parseToExternalData(_id, customData); } if ('id' in resultItem) { const { id, ...customData } = resultItem; return parseToExternalData(id, customData); } throw new ArgumentError('"_id" key missing'); } function parseToExternalData(id: unknown, customData: object) { return { systemData: { _id: parseId(id) }, customData: filterValidDataIdentifiers(customData), }; } function parseId(id: unknown) { if (typeof id === 'number') { assertValidNumericId(id); return id.toString(); } if (!isValidDataId(id)) { throw new ArgumentError('"_id" key invalid (must be numeric or hex)'); } return id; } function parseIndexResult(result: unknown): NormalIndexResult { if (!isObject(result)) { throw new ArgumentError('An index result must be an object'); } const { results: inputResults, continuation, count } = result as IndexResult; if (!Array.isArray(inputResults)) { throw new ArgumentError('Results of an index result must be an array'); } const parsedResults = inputResults.map((idOrItem) => { if (typeof idOrItem === 'number') { assertValidNumericId(idOrItem); return idOrItem; } else if (typeof idOrItem === 'string') { assertValidDataId(idOrItem); return idOrItem; } return parseResultItem(idOrItem); }); if (typeof continuation === 'string') { if (continuation.length === 0) { throw new ArgumentError( 'Continuation of an index result must be a non-empty string, null or undefined' ); } } else if (isPresent(continuation)) { throw new ArgumentError( 'Continuation of an index result must be a string, null or undefined' ); } return { results: parsedResults, continuation, count: parseCount(count), }; } function parseCount( resultCount: IndexResultCount | undefined ): number | undefined { if (resultCount === undefined || resultCount === null) return; if (typeof resultCount !== 'number' && typeof resultCount !== 'string') { throw new ArgumentError( 'Count of an index result must be a non-negative integer, null or undefined' ); } const count = Number(resultCount); if (count >= 0 && isValidInteger(count)) return count; throw new ArgumentError( 'Count of an index result must be a non-negative integer' ); } function assertValidDataId(dataId: string): asserts dataId is DataId { if (!isValidDataId(dataId)) { throw new ArgumentError( 'Strings in results of an index result must be valid data IDs' ); } } const connectionsState = createStateContainer<Record<string, UncheckedDataConnection>>(); // for test purposes only export function resetExternalDataConnections(): void { connectionsState.clear(); } export function setExternalDataConnection( name: string, partialConnection: LazyAsync<Partial<UncheckedDataConnection>> ): void { const connection = anticipatedDataConnection(partialConnection, name); connectionsState.set({ ...connectionsState.get(), [name]: connection, }); } export function hasExternalDataConnection(name: string): boolean { return !!getExternalDataConnection(name); } function getExternalDataConnection( name: string ): UncheckedDataConnection | undefined { const connections = connectionsState.get(); if (connections) return connections[name]; } export function getExternalDataConnectionNames(): string[] { const connections = connectionsState.get(); return connections ? Object.keys(connections) : []; } function getExternalDataConnectionOrThrow( name: string ): UncheckedDataConnection { const connection = getExternalDataConnection(name); if (!connection) { throw new ArgumentError(`Missing data class with name ${name}`); } return connection; } export async function getViaDataConnection( name: string, id: string ): Promise<NormalExternalData | null> { if (!isValidDataId(id)) { throw new ArgumentError(`Invalid data ID "${id}"`); } let response; try { response = await getExternalDataConnectionOrThrow(name).get(id); } catch (error) { if (error instanceof ClientError && error.httpStatus === 404) { return null; } throw error; } if (response === null) return null; if (!isObject(response)) { throw new ArgumentError('External data must be an object or null'); } return { systemData: { _id: id }, customData: filterValidDataIdentifiers(response), }; } function filterValidDataIdentifiers(data: object) { return Object.fromEntries( Object.entries(data).filter(([key]) => isValidDataIdentifier(key)) ); } export async function indexViaDataConnection( name: string, params: DataConnectionIndexParams ): Promise<NormalIndexResult> { const result = await getExternalDataConnectionOrThrow(name).index(params); if (result instanceof DataConnectionError) throw result; return parseIndexResult(result); } export async function createViaDataConnection( name: string, data: ExternalData ): Promise<NormalExternalData> { const response = await getExternalDataConnectionOrThrow(name).create(data); const { systemData, customData } = parseResultItem(response); return { systemData, customData: Object.keys(customData).length === 0 ? data : customData, }; } export async function updateViaDataConnection( name: string, id: string, data: ExternalData ): Promise<ExternalData> { const response = await getExternalDataConnectionOrThrow(name).update( id, data ); const updatedData = response ?? {}; if (!isObject(updatedData)) { throw new ArgumentError('External data must be an object or null'); } const { _id, ...filteredData } = filterValidDataIdentifiers(updatedData); return { ...data, ...filteredData }; } export function deleteViaDataConnection( name: string, id: string ): Promise<unknown> { return getExternalDataConnectionOrThrow(name).delete(id); }