docorm
Version:
Persistence layer with ORM features for JSON documents
250 lines (248 loc) • 9.21 kB
JavaScript
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