UNPKG

docorm

Version:

Persistence layer with ORM features for JSON documents

250 lines (248 loc) 9.21 kB
import _ from 'lodash'; import { arrayToDottedPath } from 'schema-fun'; import { docorm } from './index.js'; import { InternalError } from './errors.js'; import { importDirectory } from './import-dir.js'; // TODO Extract this from the actual Typescript interface somehow. const ENTITY_TYPE_KEYS = [ 'parent', 'name', 'abstract', 'restCollection', 'commonTitle', 'title', 'allowsDrafts', 'schema', 'schemaName', 'table', 'import', 'dbCallbacks', 'history', 'derivedProperties', 'restCallbacks', 'extraCollectionActions', 'extraInstanceActions' ]; const entityTypes = {}; export async function registerEntityTypes(dirPath) { await importDirectory(dirPath); } export function getEntityType(name, { required = true } = {}) { if (required && !entityTypes[name]) { throw new InternalError(`Entity type "${name}" is unknown.`); } return entityTypes[name]; } export async function getEntityTypes() { return entityTypes; } /* let loadedAllEntityTypes = false export async function getEntityTypes() { if (!loadedAllEntityTypes) { for (const entityTypeName of _.keys(unimportedEntityTypePaths)) { await getEntityType(entityTypeName) } loadedAllEntityTypes = true } return entityTypes } export async function getEntityType(entityTypeName) { let entityType = entityTypes[entityTypeName] if (!entityType && unimportedEntityTypePaths[entityTypeName]) { // console.log(`Importing file ${unimportedEntityTypePaths[entityTypeName]}`) // Using file:// makes this work with Windows paths that begin with drive letters. const {default: newlyLoadedEntityType} = await import(`file://${unimportedEntityTypePaths[entityTypeName]}`) delete unimportedEntityTypePaths[entityTypeName] entityTypes[entityTypeName] = newlyLoadedEntityType entityType = newlyLoadedEntityType } return entityType } */ function mergeCallbacks(...callbackSources) { const callbacks = {}; for (const source of callbackSources) { // TODO Eliminate "as object." _.forEach(source, (callbacksOfType, key) => { if (!callbacks[key]) { callbacks[key] = []; } callbacks[key].push(...callbacksOfType); }); } // TODO This is a crude way to return a result of the same type as the parameters. We really want to accept either // DbCallbacks or RestCallbacks and return the same type. return callbacks; } function makeParentProxy(parentName) { if (entityTypes[parentName]) { return entityTypes[parentName]; } else { return makeObjectProxy(() => getEntityType(parentName)); } } function makeObjectProxy(load) { const target = { value: undefined, loaded: false }; return new Proxy(target, { has: (target, property) => { return [...ENTITY_TYPE_KEYS, '_isProxy', '_loaded'].includes(property.toString()); }, get: (target, property) => { if (property == 'then') { return undefined; } if (property == '_isProxy') { return true; } if (property == '_loaded') { return target.loaded; } if (!target.loaded) { target.value = load(); target.loaded = true; } if (target.value) { return target.value[property]; } return undefined; }, ownKeys: (target) => target.value ? Object.keys(target.value) : [], getOwnPropertyDescriptor: (target, property) => { if (property == '_isProxy') { return { enumerable: false, value: true }; } if (property == '_loaded') { return { enumerable: false, value: target.loaded }; } /* if (!target.loaded) { target.value = load() target.loaded = true } */ if (target.value) { return Object.getOwnPropertyDescriptor(target.value, property); } return undefined; } }); } const INITIAL_BUILD_COLUMN_MAP_STATE = { knownSubschemas: {} }; function buildPropertyMappings(schema, // schemaType: SchemaType = 'model', // namespace: string | undefined = undefined, currentPath = [], state = INITIAL_BUILD_COLUMN_MAP_STATE) { let mappings = []; const { knownSubschemas } = state; const column = schema?.mapping?.column; if (schema.allOf || schema.oneOf) { const schemaOptions = (schema.allOf || schema.oneOf); const schemaOptionMaps = schemaOptions.map((schemaOption) => buildPropertyMappings(schemaOption, currentPath, state) // buildPropertyMappings(schemaOption, schemaType, namespace, currentPath, state) ); mappings = _.uniqBy(schemaOptionMaps.flat(), 'propertyPath'); } else if (schema.$ref) { if (!['ref', 'inverse-ref'].includes(schema.storage)) { const schemaRef = schema.$ref; //_.trimStart(schema.$ref, '/').split('/') const cachedSchemaKey = schema.$ref; let subschema = {}; if (state.knownSubschemas[cachedSchemaKey] != null) { subschema = knownSubschemas[cachedSchemaKey]; } else { // TODO Handle error if not found const subschema = docorm.config.schemaRegistry?.getSchema(schemaRef); if (!subschema) { throw new InternalError(`Schema refers to unknown subschema with $ref "${schemaRef}".`); } knownSubschemas[cachedSchemaKey] = subschema; } mappings = buildPropertyMappings(subschema, currentPath); } else if (schema.storage == 'ref') { if (column) { mappings.push({ propertyPath: arrayToDottedPath([...currentPath, '$ref']), column }); } } } else { switch (schema.type) { case 'object': { const properties = schema.properties; if (properties) { for (const propertyName in properties) { const partialMappings = buildPropertyMappings(properties[propertyName], [...currentPath, propertyName]); mappings.push(...partialMappings); } } } break; case 'array': // Stop here. We cannot map array elements or their properties to columns. break; default: if (column) { mappings.push({ propertyPath: arrayToDottedPath(currentPath), column }); } } } return mappings; } export function makeUnproxiedEntityType(definition) { const parentEntityType = definition.parent ? getEntityType(definition.parent) : undefined; const schema = docorm.config.schemaRegistry?.getSchema(definition.schemaId); if (!schema) { throw new InternalError(`Entity type "${definition.name}" has no schema.`); } // TODO Move definition.table to definition.mapping.table. const table = definition.table || parentEntityType?.mapping?.table; const entityType = _.merge({}, parentEntityType || {}, definition, { parent: parentEntityType, abstract: definition.abstract || false, schema, // concreteSchema: makeSchemaConcrete(schema, 'model'), mapping: undefined, dbCallbacks: mergeCallbacks(parentEntityType?.dbCallbacks || {}, definition.dbCallbacks || {}), restCallbacks: mergeCallbacks(parentEntityType?.restCallbacks || {}, definition.restCallbacks || {}) }); // Don't merge parent mappings into this entity type's mapping. entityType.mapping = table ? { table, idColumn: definition.mapping?.idColumn || 'id', jsonColumn: definition.mapping ? definition.mapping.jsonColumn : 'data', propertyMappings: buildPropertyMappings(schema), readonly: !!definition.mapping?.readonly } : undefined; return entityType; } export function makeEntityType(definition) { let entityType = undefined; if (definition.parent) { const parentEntityType = getEntityType(definition.parent, { required: false }); if (!parentEntityType) { entityType = makeObjectProxy(() => makeUnproxiedEntityType(definition)); } else { entityType = makeUnproxiedEntityType(definition); } } else { entityType = makeUnproxiedEntityType(definition); } entityTypes[definition.name] = entityType; return entityType; } export async function calculateDerivedProperties(entityType, item) { if (entityType.derivedProperties) { for (const derivedPropertyPath of _.keys(entityType.derivedProperties)) { _.set(item, derivedPropertyPath, await entityType.derivedProperties[derivedPropertyPath](item)); } } } //# sourceMappingURL=entity-types.js.map