import {
model,
isEqual,
Model,
type ModelArg,
ModelConstructor,
required,
ValidationKeys,
} from "@decaf-ts/decorator-validation";
import { Repo, Repository } from "../repository/Repository";
import { RelationsMetadata } from "./types";
import { InternalError, NotFoundError } from "@decaf-ts/db-decorators";
import { PersistenceKeys } from "../persistence/constants";
import { Cascade } from "../repository/constants";
import { Constructor, Metadata } from "@decaf-ts/decoration";
import { AdapterFlags, ContextOf } from "../persistence/types";
import { Context } from "../persistence/Context";
import { Sequence } from "../persistence/Sequence";
import { pk } from "../identity/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 AdapterFlags>(
model: M,
context: Context<F>,
alias: string,
repository?: Repo<M>,
overrides?: Record<string, any>
): Promise<M> {
const log = context.logger.for(createOrUpdate);
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>,
alias
);
log.info(`Retrieved ${repository.toString()}`);
}
repository = overrides ? repository.override(overrides) : repository;
let result: M;
if (typeof model[Model.pk(repository.class)] === "undefined") {
log.info(`No pk found in ${Model.tableName(repository.class)} - creating`);
result = await repository.create(model, context);
} else {
log.info(
`pk found in ${Model.tableName(repository.class)} - attempting update`
);
try {
result = await repository.update(model, context);
log.info(`Updated ${Model.tableName(repository.class)}`);
} catch (e: any) {
if (!(e instanceof NotFoundError)) {
throw e;
}
log.info(
`update Failed - creating new ${Model.tableName(repository.class)}`
);
result = await repository.create(model, context);
}
log.info(`After create update: ${result}`);
}
return result;
}
//
// export async function createOrUpdateBulk<
// M extends Model,
// F extends AdapterFlags,
// >(
// models: M[],
// context: Context<F>,
// alias: string,
// repository?: Repo<M>
// ): Promise<M> {
// const log = context.logger.for(createOrUpdateBulk);
// if (!repository) {
// const constructor = Model.get(models[0].constructor.name);
// if (!constructor)
// throw new InternalError(
// `Could not find model ${models[0].constructor.name}`
// );
// repository = Repository.forModel<M, Repo<M>>(
// constructor as unknown as ModelConstructor<M>,
// alias
// );
// log.info(`Retrieved ${repository.toString()}`);
// }
// const pks = models.map((m) => m[Model.pk(m)]);
//
// const existing = await Promise.allSettled(pks.map((pk) => repository.read(pk as string, context)));
//
// existing.forEach((ex, i) => {
// if (ex.status === "fulfilled") {
//
// }
// })
//
// for (let ex of existing){
// if (ex.)
// }
// let result: M;
//
// if (typeof model[Model.pk(repository.class)] === "undefined") {
// log.info(`No pk found in ${Model.tableName(repository.class)} - creating`);
// result = await repository.create(model, context);
// } else {
// log.info(
// `pk found in ${Model.tableName(repository.class)} - attempting update`
// );
// try {
// result = await repository.update(model, context);
// log.info(`Updated ${Model.tableName(repository.class)}`);
// } catch (e: any) {
// if (!(e instanceof NotFoundError)) {
// throw e;
// }
// log.info(
// `update Failed - creating new ${Model.tableName(repository.class)}`
// );
// result = await repository.create(model, context);
// }
//
// log.info(`After create update: ${result}`);
// }
// return result;
// }
/**
* @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>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M
): Promise<void> {
const propertyValue: any = model[key];
if (!propertyValue) return;
if (!validBidirectionalRelation(model, data)) return;
if (typeof propertyValue !== "object") {
const innerRepo = repositoryFromTypeMetadata(
model,
key,
this.adapter.alias
);
const read = await innerRepo.read(propertyValue, context);
await cacheModelForPopulate(context, model, key, propertyValue, read);
(model as any)[key] = propertyValue;
return;
}
const constructor: Constructor = (
typeof data.class === "function" && !data.class.name
? (data.class as () => Constructor)()
: data.class
) as Constructor;
if (!constructor)
throw new InternalError(`Could not find model ${data.class}`);
const repo: Repo<any> = Repository.forModel(constructor, this.adapter.alias);
const created = await repo
.override(this._overrides)
.create(propertyValue, context);
const pk = Model.pk(created);
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>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
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,
this.adapter.alias
);
const read = await innerRepo
.override(this._overrides)
.read(propertyValue, context);
await cacheModelForPopulate(context, model, key, propertyValue, read);
(model as any)[key] = propertyValue;
return;
}
const updated = await createOrUpdate(
model[key] as M,
context,
this.adapter.alias,
undefined,
this._overrides
);
const pk = Model.pk(updated);
await cacheModelForPopulate(
context,
model,
key,
updated[pk as keyof M] as string,
updated
);
model[key] = updated[pk as keyof M];
}
/**
* @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>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
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,
this.adapter.alias
);
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>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M
): Promise<void> {
const propertyValues: any = model[key];
if (!propertyValues || !propertyValues.length) return;
if (!validBidirectionalRelation(model, data)) 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 log = context.logger.for(oneToManyOnCreate);
const uniqueValues = new Set([...propertyValues]);
if (arrayType !== "object") {
const repo = repositoryFromTypeMetadata(model, key, this.adapter.alias);
const read = await repo.readAll([...uniqueValues.values()], context);
for (let i = 0; i < read.length; i++) {
const model = read[i];
log.info(`FOUND ONE TO MANY VALUE: ${JSON.stringify(model)}`);
await cacheModelForPopulate(
context,
model,
key,
[...uniqueValues.values()][i],
read
);
}
// for (const model of read) {
// // const read = await repo.read(id, context);
//
// }
(model as any)[key] = [...uniqueValues];
log.info(`SET ONE TO MANY IDS: ${(model as any)[key]}`);
return;
}
const pkName = Model.pk(propertyValues[0].constructor);
const result: Set<string> = new Set();
for (const m of propertyValues) {
log.info(`Creating or updating one-to-many model: ${JSON.stringify(m)}`);
const record = await createOrUpdate(
m,
context,
this.adapter.alias,
undefined,
this._overrides
);
log.info(`caching: ${JSON.stringify(record)} under ${record[pkName]}`);
await cacheModelForPopulate(context, model, key, record[pkName], record);
log.info(`Creating or updating one-to-many model: ${JSON.stringify(m)}`);
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>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
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>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
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 clazz =
typeof data.class === "function" && !data.class.name
? (data.class as any)()
: data.class;
const isInstantiated = arrayType === "object";
const repo = isInstantiated
? Repository.forModel(clazz, this.adapter.alias)
: repositoryFromTypeMetadata(model, key, this.adapter.alias);
const uniqueValues = new Set([
...(isInstantiated
? values.map((v: Record<string, any>) => v[repo["pk"] as string])
: values),
]);
const ids = [...uniqueValues.values()];
let deleted: Model[];
try {
deleted = await repo.deleteAll(ids, context);
} catch (e: unknown) {
context.logger.error(`Failed to delete all records`, e);
throw e;
}
let del: any;
for (let i = 0; i < deleted.length; i++) {
del = deleted[i];
try {
await cacheModelForPopulate(context, model, key, ids[i], del);
} catch (e: unknown) {
context.logger.error(
`Failed to cache record ${ids[i]} with key ${key as string} and model ${JSON.stringify(model, undefined, 2)} `,
e
);
throw e;
}
}
(model as any)[key] = ids;
}
export async function manyToOneOnCreate<M extends Model, R extends Repo<M>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M
): Promise<void> {
const propertyValue: any = model[key];
if (!propertyValue) return;
if (!validBidirectionalRelation(model, data)) return;
const log = context.logger.for(manyToOneOnCreate);
// If it's a primitive value (ID), read the existing record
if (typeof propertyValue !== "object") {
const innerRepo = repositoryFromTypeMetadata(
model,
key,
this.adapter.alias
);
const read = await innerRepo.read(propertyValue);
await cacheModelForPopulate(context, model, key, propertyValue, read);
(model as any)[key] = propertyValue;
return;
}
const constructor =
data.class instanceof Model ? data.class.constructor : (data.class as any);
if (!constructor)
throw new InternalError(`Could not find model ${data.class}`);
log.info(
`Creating or updating many-to-one model: ${JSON.stringify(propertyValue)}`
);
const record = await createOrUpdate(
propertyValue,
context,
this.adapter.alias
);
const pk = Model.pk(record);
log.info(`caching: ${JSON.stringify(record)} under ${record[pk]}`);
await cacheModelForPopulate(
context,
model,
key,
record[pk] as string,
record
);
(model as any)[key] = record[pk];
}
export function validBidirectionalRelation<M extends Model>(
model: M,
data: RelationsMetadata
): boolean {
let metaReverseRelation: any;
const relationConstructor =
typeof data.class === "function" && data.class.name
? data.class
: (data.class as any)();
// get the inverse relation metadata
const metaReverseRelationMeta = Metadata.get(
relationConstructor,
PersistenceKeys.RELATIONS
);
if (metaReverseRelationMeta)
metaReverseRelation = Object.values(metaReverseRelationMeta)?.find(
(rel: any) => {
const relationConstructor =
typeof rel.class === "function" && rel.class.name
? rel.class
: (rel.class as any)();
return model instanceof relationConstructor;
}
);
// If populate is set to true on both sides, we should throw an error.
if (metaReverseRelation?.populate === true && data?.populate === true) {
throw new InternalError(
"Bidirectional populate is not allowed. Please set populate to false on one side of the relation."
);
}
return true;
}
export async function manyToOneOnUpdate<M extends Model, R extends Repo<M>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M
): Promise<void> {
const { cascade } = data;
if (cascade.update !== Cascade.CASCADE) return;
return manyToOneOnCreate.apply(this as any, [
context,
data,
key as keyof Model,
model,
]);
}
export async function manyToOneOnDelete<M extends Model, R extends Repo<M>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M
): Promise<void> {
if (data.cascade.delete !== Cascade.CASCADE) return;
const value = model[key] as any;
if (!value) return;
const isInstantiated = typeof value === "object";
const repo = isInstantiated
? Repository.forModel(value, this.adapter.alias)
: repositoryFromTypeMetadata(model, key, this.adapter.alias);
const repoId = isInstantiated ? value[repo["pk"] as string] : value;
const deleted = await repo.delete(repoId);
await cacheModelForPopulate(context, model, key, repoId, deleted);
(model as any)[key] = repoId;
}
export async function manyToManyOnCreate<M extends Model, R extends Repo<M>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
modelA: M
): Promise<void> {
console.warn("DECORATOR manyToMany UNDER DEVELOPMENT");
const propertyValues: any = modelA[key];
if (!propertyValues || !propertyValues.length) return;
if (!validBidirectionalRelation(modelA, data)) 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 log = context.logger.for(manyToManyOnCreate);
const uniqueValues = new Set([...propertyValues]);
// If it's a primitive value (ID), read the existing record
if (arrayType !== "object") {
const repo = repositoryFromTypeMetadata(modelA, key, this.adapter.alias);
const read = await repo.readAll([...uniqueValues.values()], context);
for (let i = 0; i < read.length; i++) {
const model = read[i];
log.info(`FOUND MANY TO MANY VALUE: ${JSON.stringify(model)}`);
await cacheModelForPopulate(
context,
model,
key,
[...uniqueValues.values()][i],
read
);
}
// Create junction table entries
await getOrCreateJunctionModel.apply(this as Repo<Model>, [
modelA,
[...propertyValues] as any[],
log,
context,
data,
]);
(modelA as any)[key] = [...uniqueValues];
log.info(`SET MANY TO MANY IDS: ${(modelA as any)[key]}`);
return;
}
const pkName = Model.pk(propertyValues[0].constructor);
const result: Set<string> = new Set();
for (const propertyValue of propertyValues) {
log.info(
`Creating or updating many-to-many model: ${JSON.stringify(propertyValue)}`
);
const record = await createOrUpdate(
propertyValue,
context,
this.adapter.alias
);
log.info(`caching: ${JSON.stringify(record)} under ${record[pkName]}`);
await cacheModelForPopulate(context, modelA, key, record[pkName], record);
log.info(
`Creating or updating many-to-many model: ${JSON.stringify(propertyValue)}`
);
propertyValue.id = record.id;
result.add(record[pkName]);
}
// Get or generate the ID for modelA before persisting junction records
const modelPkName = Model.pk(modelA.constructor as ModelConstructor<M>);
if (typeof modelA[modelPkName] === "undefined") {
const nextId = await getNextId(this, modelA, context);
(modelA as any)[modelPkName] = nextId;
}
// Create junction table entries
const JunctionModel = await getOrCreateJunctionModel.apply(
this as Repo<Model>,
[modelA, propertyValues as Model[], log, context, data]
);
// This will require creating junction repository and storing the relationships
log.info(`Junction model created: ${JunctionModel.name}`);
(modelA as any)[key] = [...result];
}
async function getNextId<M extends Model, R extends Repo<M>>(
repo: R,
modelA: M,
context: ContextOf<R>
): Promise<string | number | bigint> {
// Get the next id for the model before it is persisted so we can put it in the junction table
const modelPkName = Model.pk(modelA.constructor as ModelConstructor<M>);
const modelAId: any = modelA[modelPkName];
if (modelAId !== undefined) {
return modelAId;
}
const pkProps = Model.sequenceFor(modelA.constructor as ModelConstructor<M>);
if (!pkProps?.name) {
pkProps.name = Model.sequenceName(modelA, "pk");
}
let sequence: Sequence;
try {
// Access adapter through the public property 'db' or use type assertion
sequence = await (repo as any).adapter.Sequence(pkProps);
return await sequence.next(context);
} catch (e: any) {
throw new InternalError(
`Failed to instantiate Sequence ${pkProps.name}: ${e}`
);
}
}
async function getOrCreateJunctionModel<M extends Model, R extends Repo<M>>(
this: R,
modelA: Model,
modelsB: Model[] | any[],
log: any,
context: ContextOf<R>,
metadata?: RelationsMetadata
): Promise<Constructor<Model<false>>> {
const { JunctionModel, fkA, fkB } = getAndConstructJunctionTable(
modelA,
modelsB[0],
metadata
);
const recordIds: any[] = [];
for (const modelB of modelsB) {
log.info(
`Creating or updating many-to-many junction model: ${JSON.stringify(modelB)}`
);
// If it is a model, find and store fk content, else it is fk value directly
const junctionRegister = {
[fkA]:
modelA instanceof Model
? modelA[
Model.pk(modelA.constructor as Constructor) as keyof typeof modelA
]
: modelA,
[fkB]:
modelB instanceof Model
? modelB[
Model.pk(modelB.constructor as Constructor) as keyof typeof modelB
]
: modelB,
};
const record: any = await createOrUpdate(
new JunctionModel(junctionRegister),
context,
this.adapter.alias
);
if (record?.id) recordIds.push(record.id);
}
if (recordIds.length === modelsB?.length) {
console.log(
`All junction records created successfully for table ${JunctionModel?.name}`
);
const repository = Repository.forModel<M, Repo<M>>(
JunctionModel as unknown as ModelConstructor<M>
);
const results = await repository?.readAll(recordIds);
console.log("results:", results);
} else
console.error(
`Some junction records failed to be created for table ${JunctionModel?.name}`
);
return JunctionModel;
}
export function getAndConstructJunctionTable(
modelA: Model,
modelB: Model | any,
metadata?: RelationsMetadata
): { fkA: string; fkB: string; JunctionModel: Constructor<Model<false>> } {
// Get the name of the table and fks
const modelAName = Model.tableName(modelA);
let modelBName;
if (modelB instanceof Model) modelBName = Model.tableName(modelB);
else if (
Model.isModel(modelB as Record<string, any>) &&
typeof modelB === "function"
) {
modelBName = modelB.name ? modelB.name : (modelB as any)()?.name;
} else if (metadata?.class) {
const clazz =
typeof metadata.class === "function" && !metadata.class.name
? (metadata.class as any)()
: metadata.class;
modelBName = Model.tableName(clazz);
}
if (!modelAName || !modelBName)
throw new InternalError("Missing tablenames to create junction table");
const junctionTableName = metadata?.joinTable?.name
? metadata?.joinTable?.name
: `${modelAName}_${modelBName}`;
const fkA = `${modelAName?.toLowerCase()}_fk`;
const fkB = `${modelBName?.toLowerCase()}_fk`;
// Anonymous class to be able to change name
const DynamicJunctionModel = class extends Model {
id!: number;
[fkA]!: number;
[fkB]!: number;
constructor(arg?: ModelArg<any>) {
super(arg);
}
};
Object.defineProperty(DynamicJunctionModel, "name", {
value: junctionTableName,
writable: false,
});
// Apply the decorators
pk({ type: Number })(DynamicJunctionModel.prototype, "id");
required()(DynamicJunctionModel.prototype, fkA as any);
required()(DynamicJunctionModel.prototype, fkB as any);
// Apply @model() decorator to the class
const DecoratedModel = model()(DynamicJunctionModel);
Metadata.set(DynamicJunctionModel, PersistenceKeys.TABLE, junctionTableName);
return {
fkA,
fkB,
JunctionModel: DecoratedModel as Constructor<Model<false>>,
};
}
export async function manyToManyOnUpdate<M extends Model, R extends Repo<M>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M
): Promise<void> {
console.warn("method not yet implemented");
const { cascade } = data;
if (cascade.update !== Cascade.CASCADE) return;
return manyToManyOnCreate.apply(this as any, [
context,
data,
key as keyof Model,
model,
]);
}
export async function manyToManyOnDelete<M extends Model, R extends Repo<M>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M
): Promise<void> {
console.warn("Method under development");
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.`
);
// Delete the values and the junction table entries
const clazz =
typeof data.class === "function" && !data.class.name
? (data.class as any)()
: data.class;
const isInstantiated = arrayType === "object";
const repo = isInstantiated
? Repository.forModel(clazz, this.adapter.alias)
: repositoryFromTypeMetadata(model, key, this.adapter.alias);
const uniqueValues = new Set([
...(isInstantiated
? values.map((v: Record<string, any>) => v[repo["pk"] as string])
: values),
]);
const ids = [...uniqueValues.values()];
let deleted: Model[];
try {
deleted = await repo.deleteAll(ids, context);
} catch (e: unknown) {
context.logger.error(`Failed to delete all records`, e);
throw e;
}
let del: any;
for (let i = 0; i < deleted.length; i++) {
del = deleted[i];
try {
await cacheModelForPopulate(context, model, key, ids[i], del);
} catch (e: unknown) {
context.logger.error(
`Failed to cache record ${ids[i]} with key ${key as string} and model ${JSON.stringify(model, undefined, 2)} `,
e
);
throw e;
}
}
(model as any)[key] = ids;
}
/**
* @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(".");
}
export function getTagForDeleteKey(
tableName: string,
fieldName: string,
id: string | number
) {
return [PersistenceKeys.TAG_FOR_DELETION, tableName, 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 AdapterFlags,
>(
context: Context<F>,
parentModel: M,
propertyKey: keyof M | string,
pkValue: string | number,
cacheValue: any
) {
const cacheKey = getPopulateKey(
parentModel.constructor.name,
propertyKey as string,
pkValue
);
const cache = context.get("cacheForPopulate") || {};
(cache[cacheKey] as Record<string, any>) = cacheValue;
return context.accumulate({ cacheForPopulate: cache } as any);
}
/**
* @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>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
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: ContextOf<R>,
model: M,
propName: string,
propKeyValues: any[]
) {
let cacheKey: string;
let val: any;
const results: M[] = [];
const cache = c.get("cacheForPopulate") || {};
for (const proKeyValue of propKeyValues) {
cacheKey = getPopulateKey(model.constructor.name, propName, proKeyValue);
try {
val = cache[cacheKey];
if (!val) throw new Error("Not found in cache");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {
const repo = repositoryFromTypeMetadata(model, propName as keyof M);
if (!repo) throw new InternalError("Could not find repo");
val = await repo.read(proKeyValue, context);
}
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];
}
export async function cascadeDelete<M extends Model, R extends Repo<M>>(
this: R,
context: ContextOf<R>,
data: RelationsMetadata,
key: keyof M,
model: M,
oldModel: M
): Promise<void> {
if (data.cascade.update !== Cascade.CASCADE) return;
const nested: any = model[key];
const isArr = Array.isArray(nested);
if (typeof nested === "undefined" || (isArr && nested.length === 0)) return;
if (!oldModel)
throw new InternalError(
"No way to compare old model. do you have updateValidation and mergeModels enabled?"
);
function reduceToPk(obj: any): any {
if (Array.isArray(obj)) return obj.map(reduceToPk);
return typeof obj !== "object" ? obj : obj[Model.pk(obj)];
}
const newVal = reduceToPk(model[key]);
const oldVal = reduceToPk(oldModel[key]);
if (typeof oldVal === "undefined" || isEqual(newVal, oldVal)) {
return;
}
if (Array.isArray(newVal) !== Array.isArray(oldVal))
throw new InternalError(`Cannot cascade update for different array types`);
const newToCompare = (Array.isArray(newVal) ? newVal : [newVal]).filter(
Boolean
) as any[];
const oldToCompare = (Array.isArray(oldVal) ? oldVal : [oldVal]).filter(
Boolean
) as any[];
const toDelete = (oldToCompare as any[]).filter(
(v) => !(newToCompare as any[]).includes(v)
);
const repo = repositoryFromTypeMetadata(model, key as keyof M);
if (!repo) throw new InternalError("Could not find repo");
console.log("herehere");
try {
const deleted = await repo.deleteAll(toDelete, context);
context.logger.debug(
`Deleted ${deleted.length} entries from table ${Model.tableName(repo.class)} due to cascade rules with `
);
} catch (e: unknown) {
throw new InternalError(`Error deleting cascade entries: ${e}`);
}
}
/**
* @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->>repositoryFromTypeMetadata: Get 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: M,
propertyKey: keyof M,
alias?: string
): Repo<M> {
if (!model) throw new Error("No model was provided to get repository");
let allowedTypes;
if (Array.isArray(model[propertyKey]) || model[propertyKey] instanceof Set) {
const customTypes = Metadata.get(
model instanceof Model ? model.constructor : (model as any),
Metadata.key(
ValidationKeys.REFLECT,
propertyKey as string,
ValidationKeys.LIST
)
)?.clazz;
if (!customTypes)
throw new InternalError(
`Failed to find types decorators for property ${propertyKey as string}`
);
allowedTypes = (
Array.isArray(customTypes) ? [...customTypes] : [customTypes]
).map((t) => (typeof t === "function" && !(t as any).name ? t() : t));
} else
allowedTypes = Metadata.getPropDesignTypes(
model instanceof Model ? model.constructor : (model as any),
propertyKey as string
)?.designTypes;
const constructor = allowedTypes?.find(
(t) => !commomTypes.includes(`${t.name}`.toLowerCase())
);
return Repository.forModel(constructor, alias) as any;
}
Source