import {
Constructor,
Model,
ModelConstructor,
Validation,
ValidationKeys,
} from "@decaf-ts/decorator-validation";
import { Repo, Repository } from "../repository/Repository";
import { RelationsMetadata } from "./types";
import {
findPrimaryKey,
InternalError,
NotFoundError,
RepositoryFlags,
} from "@decaf-ts/db-decorators";
import { PersistenceKeys } from "../persistence/constants";
import { Cascade } from "../repository/constants";
import { Context } from "@decaf-ts/db-decorators";
/**
* @description Creates or updates a model instance
* @summary Determines whether to create a new model or update an existing one based on the presence of a primary key
* @template M - The model type extending Model
* @template F - The repository flags type
* @param {M} model - The model instance to create or update
* @param {Context<F>} context - The context for the operation
* @param {Repo<M, F, Context<F>>} [repository] - Optional repository to use for the operation
* @return {Promise<M>} A promise that resolves to the created or updated model
* @function createOrUpdate
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant createOrUpdate
* participant Repository
* participant Model
*
* Caller->>createOrUpdate: model, context, repository?
* alt repository not provided
* createOrUpdate->>Model: get(model.constructor.name)
* Model-->>createOrUpdate: constructor
* createOrUpdate->>Repository: forModel(constructor)
* Repository-->>createOrUpdate: repository
* end
*
* alt primary key undefined
* createOrUpdate->>Repository: create(model, context)
* Repository-->>createOrUpdate: created model
* else primary key defined
* createOrUpdate->>Repository: update(model, context)
* alt update successful
* Repository-->>createOrUpdate: updated model
* else NotFoundError
* createOrUpdate->>Repository: create(model, context)
* Repository-->>createOrUpdate: created model
* end
* end
*
* createOrUpdate-->>Caller: model
*/
export async function createOrUpdate<
M extends Model,
F extends RepositoryFlags,
>(
model: M,
context: Context<F>,
repository?: Repo<M, F, Context<F>>
): Promise<M> {
if (!repository) {
const constructor = Model.get(model.constructor.name);
if (!constructor)
throw new InternalError(`Could not find model ${model.constructor.name}`);
repository = Repository.forModel<M, Repo<M>>(
constructor as unknown as ModelConstructor<M>
);
}
if (typeof model[repository.pk] === "undefined")
return repository.create(model, context);
else {
try {
return repository.update(model, context);
} catch (e: any) {
if (!(e instanceof NotFoundError)) throw e;
return repository.create(model, context);
}
}
}
/**
* @description Handles one-to-one relationship creation
* @summary Processes a one-to-one relationship when creating a model, either by referencing an existing model or creating a new one
* @template M - The model type extending Model
* @template R - The repository type extending Repo<M, F, C>
* @template V - The relations metadata type extending RelationsMetadata
* @template F - The repository flags type
* @template C - The context type extending Context<F>
* @param {R} this - The repository instance
* @param {Context<F>} context - The context for the operation
* @param {V} data - The relations metadata
* @param {string} key - The property key of the relationship
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function oneToOneOnCreate
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant oneToOneOnCreate
* participant repositoryFromTypeMetadata
* participant Model
* participant Repository
* participant cacheModelForPopulate
*
* Caller->>oneToOneOnCreate: this, context, data, key, model
* oneToOneOnCreate->>oneToOneOnCreate: check if propertyValue exists
*
* alt propertyValue is not an object
* oneToOneOnCreate->>repositoryFromTypeMetadata: model, key
* repositoryFromTypeMetadata-->>oneToOneOnCreate: innerRepo
* oneToOneOnCreate->>innerRepo: read(propertyValue)
* innerRepo-->>oneToOneOnCreate: read
* oneToOneOnCreate->>cacheModelForPopulate: context, model, key, propertyValue, read
* oneToOneOnCreate->>oneToOneOnCreate: set model[key] = propertyValue
* else propertyValue is an object
* oneToOneOnCreate->>Model: get(data.class)
* Model-->>oneToOneOnCreate: constructor
* oneToOneOnCreate->>Repository: forModel(constructor)
* Repository-->>oneToOneOnCreate: repo
* oneToOneOnCreate->>repo: create(propertyValue)
* repo-->>oneToOneOnCreate: created
* oneToOneOnCreate->>findPrimaryKey: created
* findPrimaryKey-->>oneToOneOnCreate: pk
* oneToOneOnCreate->>cacheModelForPopulate: context, model, key, created[pk], created
* oneToOneOnCreate->>oneToOneOnCreate: set model[key] = created[pk]
* end
*
* oneToOneOnCreate-->>Caller: void
*/
export async function oneToOneOnCreate<
M extends Model,
R extends Repo<M, F, C>,
V extends RelationsMetadata,
F extends RepositoryFlags,
C extends Context<F>,
>(
this: R,
context: Context<F>,
data: V,
key: keyof M,
model: M
): Promise<void> {
const propertyValue: any = model[key];
if (!propertyValue) return;
if (typeof propertyValue !== "object") {
const innerRepo = repositoryFromTypeMetadata(model, key);
const read = await innerRepo.read(propertyValue);
await cacheModelForPopulate(context, model, key, propertyValue, read);
(model as any)[key] = propertyValue;
return;
}
const constructor = Model.get(data.class);
if (!constructor)
throw new InternalError(`Could not find model ${data.class}`);
const repo: Repo<any> = Repository.forModel(constructor);
const created = await repo.create(propertyValue);
const pk = findPrimaryKey(created).id;
await cacheModelForPopulate(context, model, key, created[pk], created);
(model as any)[key] = created[pk];
}
/**
* @description Handles one-to-one relationship updates
* @summary Processes a one-to-one relationship when updating a model, either by referencing an existing model or updating the related model
* @template M - The model type extending Model
* @template R - The repository type extending Repo<M, F, C>
* @template V - The relations metadata type extending RelationsMetadata
* @template F - The repository flags type
* @template C - The context type extending Context<F>
* @param {R} this - The repository instance
* @param {Context<F>} context - The context for the operation
* @param {V} data - The relations metadata
* @param key - The property key of the relationship
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function oneToOneOnUpdate
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant oneToOneOnUpdate
* participant repositoryFromTypeMetadata
* participant createOrUpdate
* participant findPrimaryKey
* participant cacheModelForPopulate
*
* Caller->>oneToOneOnUpdate: this, context, data, key, model
* oneToOneOnUpdate->>oneToOneOnUpdate: check if propertyValue exists
* oneToOneOnUpdate->>oneToOneOnUpdate: check if cascade.update is CASCADE
*
* alt propertyValue is not an object
* oneToOneOnUpdate->>repositoryFromTypeMetadata: model, key
* repositoryFromTypeMetadata-->>oneToOneOnUpdate: innerRepo
* oneToOneOnUpdate->>innerRepo: read(propertyValue)
* innerRepo-->>oneToOneOnUpdate: read
* oneToOneOnUpdate->>cacheModelForPopulate: context, model, key, propertyValue, read
* oneToOneOnUpdate->>oneToOneOnUpdate: set model[key] = propertyValue
* else propertyValue is an object
* oneToOneOnUpdate->>createOrUpdate: model[key], context
* createOrUpdate-->>oneToOneOnUpdate: updated
* oneToOneOnUpdate->>findPrimaryKey: updated
* findPrimaryKey-->>oneToOneOnUpdate: pk
* oneToOneOnUpdate->>cacheModelForPopulate: context, model, key, updated[pk], updated
* oneToOneOnUpdate->>oneToOneOnUpdate: set model[key] = updated[pk]
* end
*
* oneToOneOnUpdate-->>Caller: void
*/
export async function oneToOneOnUpdate<
M extends Model,
R extends Repo<M, F, C>,
V extends RelationsMetadata,
F extends RepositoryFlags,
C extends Context<F>,
>(
this: R,
context: Context<F>,
data: V,
key: keyof M,
model: M
): Promise<void> {
const propertyValue: any = model[key];
if (!propertyValue) return;
if (data.cascade.update !== Cascade.CASCADE) return;
if (typeof propertyValue !== "object") {
const innerRepo = repositoryFromTypeMetadata(model, key);
const read = await innerRepo.read(propertyValue);
await cacheModelForPopulate(context, model, key, propertyValue, read);
(model as any)[key] = propertyValue;
return;
}
const updated = await createOrUpdate(model[key] as M, context);
const pk = findPrimaryKey(updated).id;
await cacheModelForPopulate(
context,
model,
key,
updated[pk] as string,
updated
);
model[key] = updated[pk];
}
/**
* @description Handles one-to-one relationship deletion
* @summary Processes a one-to-one relationship when deleting a model, deleting the related model if cascade is enabled
* @template M - The model type extending Model
* @template R - The repository type extending Repo<M, F, C>
* @template V - The relations metadata type extending RelationsMetadata
* @template F - The repository flags type
* @template C - The context type extending Context<F>
* @param {R} this - The repository instance
* @param {Context<F>} context - The context for the operation
* @param {V} data - The relations metadata
* @param key - The property key of the relationship
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function oneToOneOnDelete
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant oneToOneOnDelete
* participant repositoryFromTypeMetadata
* participant cacheModelForPopulate
*
* Caller->>oneToOneOnDelete: this, context, data, key, model
* oneToOneOnDelete->>oneToOneOnDelete: check if propertyValue exists
* oneToOneOnDelete->>oneToOneOnDelete: check if cascade.update is CASCADE
*
* oneToOneOnDelete->>repositoryFromTypeMetadata: model, key
* repositoryFromTypeMetadata-->>oneToOneOnDelete: innerRepo
*
* alt propertyValue is not a Model instance
* oneToOneOnDelete->>innerRepo: delete(model[key], context)
* innerRepo-->>oneToOneOnDelete: deleted
* else propertyValue is a Model instance
* oneToOneOnDelete->>innerRepo: delete(model[key][innerRepo.pk], context)
* innerRepo-->>oneToOneOnDelete: deleted
* end
*
* oneToOneOnDelete->>cacheModelForPopulate: context, model, key, deleted[innerRepo.pk], deleted
* oneToOneOnDelete-->>Caller: void
*/
export async function oneToOneOnDelete<
M extends Model,
R extends Repo<M, F, C>,
V extends RelationsMetadata,
F extends RepositoryFlags,
C extends Context<F>,
>(
this: R,
context: Context<F>,
data: V,
key: keyof M,
model: M
): Promise<void> {
const propertyValue: any = model[key];
if (!propertyValue) return;
if (data.cascade.update !== Cascade.CASCADE) return;
const innerRepo: Repo<M> = repositoryFromTypeMetadata(model, key);
let deleted: M;
if (!(propertyValue instanceof Model))
deleted = await innerRepo.delete(model[key] as string, context);
else
deleted = await innerRepo.delete(
(model[key] as M)[innerRepo.pk as keyof M] as string,
context
);
await cacheModelForPopulate(
context,
model,
key,
deleted[innerRepo.pk] as string,
deleted
);
}
/**
* @description Handles one-to-many relationship creation
* @summary Processes a one-to-many relationship when creating a model, either by referencing existing models or creating new ones
* @template M - The model type extending Model
* @template R - The repository type extending Repo<M, F, C>
* @template V - The relations metadata type extending RelationsMetadata
* @template F - The repository flags type
* @template C - The context type extending Context<F>
* @param {R} this - The repository instance
* @param {Context<F>} context - The context for the operation
* @param {V} data - The relations metadata
* @param key - The property key of the relationship
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function oneToManyOnCreate
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant oneToManyOnCreate
* participant repositoryFromTypeMetadata
* participant createOrUpdate
* participant findPrimaryKey
* participant cacheModelForPopulate
*
* Caller->>oneToManyOnCreate: this, context, data, key, model
* oneToManyOnCreate->>oneToManyOnCreate: check if propertyValues exists and has length
* oneToManyOnCreate->>oneToManyOnCreate: check if all elements have same type
* oneToManyOnCreate->>oneToManyOnCreate: create uniqueValues set
*
* alt arrayType is not "object"
* oneToManyOnCreate->>repositoryFromTypeMetadata: model, key
* repositoryFromTypeMetadata-->>oneToManyOnCreate: repo
* loop for each id in uniqueValues
* oneToManyOnCreate->>repo: read(id)
* repo-->>oneToManyOnCreate: read
* oneToManyOnCreate->>cacheModelForPopulate: context, model, key, id, read
* end
* oneToManyOnCreate->>oneToManyOnCreate: set model[key] = [...uniqueValues]
* else arrayType is "object"
* oneToManyOnCreate->>findPrimaryKey: propertyValues[0]
* findPrimaryKey-->>oneToManyOnCreate: pkName
* oneToManyOnCreate->>oneToManyOnCreate: create result set
* loop for each m in propertyValues
* oneToManyOnCreate->>createOrUpdate: m, context
* createOrUpdate-->>oneToManyOnCreate: record
* oneToManyOnCreate->>cacheModelForPopulate: context, model, key, record[pkName], record
* oneToManyOnCreate->>oneToManyOnCreate: add record[pkName] to result
* end
* oneToManyOnCreate->>oneToManyOnCreate: set model[key] = [...result]
* end
*
* oneToManyOnCreate-->>Caller: void
*/
export async function oneToManyOnCreate<
M extends Model,
R extends Repo<M, F, C>,
V extends RelationsMetadata,
F extends RepositoryFlags,
C extends Context<F>,
>(
this: R,
context: Context<F>,
data: V,
key: keyof M,
model: M
): Promise<void> {
const propertyValues: any = model[key];
if (!propertyValues || !propertyValues.length) return;
const arrayType = typeof propertyValues[0];
if (!propertyValues.every((item: any) => typeof item === arrayType))
throw new InternalError(
`Invalid operation. All elements of property ${key as string} must match the same type.`
);
const uniqueValues = new Set([...propertyValues]);
if (arrayType !== "object") {
const repo = repositoryFromTypeMetadata(model, key);
for (const id of uniqueValues) {
const read = await repo.read(id);
await cacheModelForPopulate(context, model, key, id, read);
}
(model as any)[key] = [...uniqueValues];
return;
}
const pkName = findPrimaryKey(propertyValues[0]).id;
const result: Set<string> = new Set();
for (const m of propertyValues) {
const record = await createOrUpdate(m, context);
await cacheModelForPopulate(context, model, key, record[pkName], record);
result.add(record[pkName]);
}
(model as any)[key] = [...result];
}
/**
* @description Handles one-to-many relationship updates
* @summary Processes a one-to-many relationship when updating a model, delegating to oneToManyOnCreate if cascade update is enabled
* @template M - The model type extending Model
* @template R - The repository type extending Repo<M, F, C>
* @template V - The relations metadata type extending RelationsMetadata
* @template F - The repository flags type
* @template C - The context type extending Context<F>
* @param {R} this - The repository instance
* @param {Context<F>} context - The context for the operation
* @param {V} data - The relations metadata
* @param key - The property key of the relationship
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function oneToManyOnUpdate
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant oneToManyOnUpdate
* participant oneToManyOnCreate
*
* Caller->>oneToManyOnUpdate: this, context, data, key, model
* oneToManyOnUpdate->>oneToManyOnUpdate: check if cascade.update is CASCADE
*
* alt cascade.update is CASCADE
* oneToManyOnUpdate->>oneToManyOnCreate: apply(this, [context, data, key, model])
* oneToManyOnCreate-->>oneToManyOnUpdate: void
* end
*
* oneToManyOnUpdate-->>Caller: void
*/
export async function oneToManyOnUpdate<
M extends Model,
R extends Repo<M, F, C>,
V extends RelationsMetadata,
F extends RepositoryFlags,
C extends Context<F>,
>(
this: R,
context: Context<F>,
data: V,
key: keyof M,
model: M
): Promise<void> {
const { cascade } = data;
if (cascade.update !== Cascade.CASCADE) return;
return oneToManyOnCreate.apply(this as any, [
context,
data,
key as keyof Model,
model,
]);
}
/**
* @description Handles one-to-many relationship deletion
* @summary Processes a one-to-many relationship when deleting a model, deleting all related models if cascade delete is enabled
* @template M - The model type extending Model
* @template R - The repository type extending Repo<M, F, C>
* @template V - The relations metadata type extending RelationsMetadata
* @template F - The repository flags type
* @template C - The context type extending Context<F>
* @param {R} this - The repository instance
* @param {Context<F>} context - The context for the operation
* @param {V} data - The relations metadata
* @param key - The property key of the relationship
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function oneToManyOnDelete
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant oneToManyOnDelete
* participant Repository
* participant repositoryFromTypeMetadata
* participant cacheModelForPopulate
*
* Caller->>oneToManyOnDelete: this, context, data, key, model
* oneToManyOnDelete->>oneToManyOnDelete: check if cascade.delete is CASCADE
* oneToManyOnDelete->>oneToManyOnDelete: check if values exists and has length
* oneToManyOnDelete->>oneToManyOnDelete: check if all elements have same type
*
* alt isInstantiated (arrayType is "object")
* oneToManyOnDelete->>Repository: forModel(values[0])
* Repository-->>oneToManyOnDelete: repo
* else not instantiated
* oneToManyOnDelete->>repositoryFromTypeMetadata: model, key
* repositoryFromTypeMetadata-->>oneToManyOnDelete: repo
* end
*
* oneToManyOnDelete->>oneToManyOnDelete: create uniqueValues set
*
* loop for each id in uniqueValues
* oneToManyOnDelete->>repo: delete(id, context)
* repo-->>oneToManyOnDelete: deleted
* oneToManyOnDelete->>cacheModelForPopulate: context, model, key, id, deleted
* end
*
* oneToManyOnDelete->>oneToManyOnDelete: set model[key] = [...uniqueValues]
* oneToManyOnDelete-->>Caller: void
*/
export async function oneToManyOnDelete<
M extends Model,
R extends Repo<M, F, C>,
V extends RelationsMetadata,
F extends RepositoryFlags,
C extends Context<F>,
>(
this: R,
context: Context<F>,
data: V,
key: keyof M,
model: M
): Promise<void> {
if (data.cascade.delete !== Cascade.CASCADE) return;
const values = model[key] as any;
if (!values || !values.length) return;
const arrayType = typeof values[0];
const areAllSameType = values.every((item: any) => typeof item === arrayType);
if (!areAllSameType)
throw new InternalError(
`Invalid operation. All elements of property ${key as string} must match the same type.`
);
const isInstantiated = arrayType === "object";
const repo = isInstantiated
? Repository.forModel(values[0],this.adapter.alias)
: repositoryFromTypeMetadata(model, key);
const uniqueValues = new Set([
...(isInstantiated
? values.map((v: Record<string, any>) => v[repo.pk as string])
: values),
]);
for (const id of uniqueValues.values()) {
const deleted = await repo.delete(id, context);
await cacheModelForPopulate(context, model, key, id, deleted);
}
(model as any)[key] = [...uniqueValues];
}
/**
* @description Generates a key for caching populated model relationships
* @summary Creates a unique key for storing and retrieving populated model relationships in the cache
* @param {string} tableName - The name of the table or model
* @param {string} fieldName - The name of the field or property
* @param {string|number} id - The identifier of the related model
* @return {string} A dot-separated string that uniquely identifies the relationship
* @function getPopulateKey
* @memberOf module:core
*/
export function getPopulateKey(
tableName: string,
fieldName: string,
id: string | number
) {
return [PersistenceKeys.POPULATE, tableName, fieldName, id].join(".");
}
/**
* @description Caches a model for later population
* @summary Stores a model in the context cache for efficient retrieval during relationship population
* @template M - The model type extending Model
* @template F - The repository flags type
* @param {Context<F>} context - The context for the operation
* @param {M} parentModel - The parent model that contains the relationship
* @param propertyKey - The property key of the relationship
* @param {string | number} pkValue - The primary key value of the related model
* @param {any} cacheValue - The model instance to cache
* @return {Promise<any>} A promise that resolves with the result of the cache operation
* @function cacheModelForPopulate
* @memberOf module:core
*/
export async function cacheModelForPopulate<
M extends Model,
F extends RepositoryFlags,
>(
context: Context<F>,
parentModel: M,
propertyKey: keyof M | string,
pkValue: string | number,
cacheValue: any
) {
const cacheKey = getPopulateKey(
parentModel.constructor.name,
propertyKey as string,
pkValue
);
return context.accumulate({ [cacheKey]: cacheValue });
}
/**
* @description Populates a model's relationship
* @summary Retrieves and attaches related models to a model's relationship property
* @template M - The model type extending Model
* @template R - The repository type extending Repo<M, F, C>
* @template V - The relations metadata type extending RelationsMetadata
* @template F - The repository flags type
* @template C - The context type extending Context<F>
* @param {R} this - The repository instance
* @param {Context<F>} context - The context for the operation
* @param {V} data - The relations metadata
* @param key - The property key of the relationship
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the operation is complete
* @function populate
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant populate
* participant fetchPopulateValues
* participant getPopulateKey
* participant Context
* participant repositoryFromTypeMetadata
*
* Caller->>populate: this, context, data, key, model
* populate->>populate: check if data.populate is true
* populate->>populate: get nested value and check if it exists
*
* populate->>fetchPopulateValues: context, model, key, isArr ? nested : [nested]
*
* fetchPopulateValues->>fetchPopulateValues: initialize variables
*
* loop for each proKeyValue in propKeyValues
* fetchPopulateValues->>getPopulateKey: model.constructor.name, propName, proKeyValue
* getPopulateKey-->>fetchPopulateValues: cacheKey
*
* alt try to get from cache
* fetchPopulateValues->>Context: get(cacheKey)
* Context-->>fetchPopulateValues: val
* else catch error
* fetchPopulateValues->>repositoryFromTypeMetadata: model, propName
* repositoryFromTypeMetadata-->>fetchPopulateValues: repo
* fetchPopulateValues->>repo: read(proKeyValue)
* repo-->>fetchPopulateValues: val
* end
*
* fetchPopulateValues->>fetchPopulateValues: add val to results
* end
*
* fetchPopulateValues-->>populate: results
* populate->>populate: set model[key] = isArr ? res : res[0]
* populate-->>Caller: void
*/
export async function populate<
M extends Model,
R extends Repo<M, F, C>,
V extends RelationsMetadata,
F extends RepositoryFlags,
C extends Context<F>,
>(
this: R,
context: Context<F>,
data: V,
key: keyof M,
model: M
): Promise<void> {
if (!data.populate) return;
const nested: any = model[key];
const isArr = Array.isArray(nested);
if (typeof nested === "undefined" || (isArr && nested.length === 0)) return;
async function fetchPopulateValues(
c: Context<F>,
model: M,
propName: string,
propKeyValues: any[]
) {
let cacheKey: string;
let val: any;
const results: M[] = [];
for (const proKeyValue of propKeyValues) {
cacheKey = getPopulateKey(model.constructor.name, propName, proKeyValue);
try {
val = await c.get(cacheKey as any);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {
const repo = repositoryFromTypeMetadata(model, propName);
if (!repo) throw new InternalError("Could not find repo");
val = await repo.read(proKeyValue);
}
results.push(val);
}
return results;
}
const res = await fetchPopulateValues(
context,
model,
key as string,
isArr ? nested : [nested]
);
(model as any)[key] = isArr ? res : res[0];
}
/**
* @description List of common JavaScript types
* @summary An array of strings representing common JavaScript types that are not custom model types
* @const commomTypes
* @memberOf module:core
*/
const commomTypes = [
"array",
"string",
"number",
"boolean",
"symbol",
"function",
"object",
"undefined",
"null",
"bigint",
];
/**
* @description Retrieves a repository for a model property based on its type metadata
* @summary Examines a model property's type metadata to determine the appropriate repository for related models
* @template M - The model type extending Model
* @param {any} model - The model instance containing the property
* @param propertyKey - The property key to examine
* @return {Repo<M>} A repository for the model type associated with the property
* @function repositoryFromTypeMetadata
* @memberOf module:core
* @mermaid
* sequenceDiagram
* participant Caller
* participant repositoryFromTypeMetadata
* participant Reflect
* participant Validation
* participant Model
* participant Repository
*
* Caller->>repositoryFromTypeMetadata: model, propertyKey
*
* repositoryFromTypeMetadata->>Validation: key(Array.isArray(model[propertyKey]) ? ValidationKeys.LIST : ValidationKeys.TYPE)
* Validation-->>repositoryFromTypeMetadata: validationKey
*
* repositoryFromTypeMetadata->>Reflect: getMetadata(validationKey, model, propertyKey)
* Reflect-->>repositoryFromTypeMetadata: types
*
* repositoryFromTypeMetadata->>repositoryFromTypeMetadata: determine customTypes based on property type
* repositoryFromTypeMetadata->>repositoryFromTypeMetadata: check if types and customTypes exist
*
* repositoryFromTypeMetadata->>repositoryFromTypeMetadata: create allowedTypes array
* repositoryFromTypeMetadata->>repositoryFromTypeMetadata: find constructorName not in commomTypes
* repositoryFromTypeMetadata->>repositoryFromTypeMetadata: check if constructorName exists
*
* repositoryFromTypeMetadata->>Model: get(constructorName)
* Model-->>repositoryFromTypeMetadata: constructor
* repositoryFromTypeMetadata->>repositoryFromTypeMetadata: check if constructor exists
*
* repositoryFromTypeMetadata->>Repository: forModel(constructor)
* Repository-->>repositoryFromTypeMetadata: repo
*
* repositoryFromTypeMetadata-->>Caller: repo
*/
export function repositoryFromTypeMetadata<M extends Model>(
model: any,
propertyKey: string | keyof M
): Repo<M> {
const types = Reflect.getMetadata(
Validation.key(
Array.isArray(model[propertyKey])
? ValidationKeys.LIST
: ValidationKeys.TYPE
),
model,
propertyKey as string
);
const customTypes: any = Array.isArray(model[propertyKey])
? types.clazz
: types.customTypes;
if (!types || !customTypes)
throw new InternalError(
`Failed to find types decorators for property ${propertyKey as string}`
);
const allowedTypes: string[] = Array.isArray(customTypes)
? [...customTypes]
: [customTypes];
const constructorName = allowedTypes.find(
(t) => !commomTypes.includes(`${t}`.toLowerCase())
);
if (!constructorName)
throw new InternalError(
`Property key ${propertyKey as string} does not have a valid constructor type`
);
const constructor: Constructor<M> | undefined = Model.get(constructorName);
if (!constructor)
throw new InternalError(`No registered model found for ${constructorName}`);
return Repository.forModel(constructor);
}
Source