import "../overrides";
import { Model, required } from "@decaf-ts/decorator-validation";
import {
DefaultSequenceOptions,
SequenceOptions,
} from "../interfaces/SequenceOptions";
import {
DBKeys,
generated,
GroupSort,
InternalError,
onCreate,
onUpdate,
readonly,
} from "@decaf-ts/db-decorators";
import type { Sequence } from "../persistence/Sequence";
import { PersistenceKeys } from "../persistence/constants";
import { OrderDirection } from "../repository/constants";
import {
apply,
Decoration,
Metadata,
prop,
propMetadata,
} from "@decaf-ts/decoration";
import { Repository } from "../repository/Repository";
import { ContextOf } from "../persistence/types";
import { index } from "../model/indexing";
const defaultPkPriority = 60; // Default priority for primary key to run latter than other properties
type SequenceNameFn = (model: any, ...args: string[]) => string;
export type SequenceDecoratorParams = {
/** when true, handler also runs on update (generates the next value) */
update?: boolean;
};
function isSequenceNameFn(fn: unknown): fn is SequenceNameFn {
return typeof fn === "function";
}
function resolveSequenceName(model: any, suffix: string) {
if (isSequenceNameFn(Model.sequenceName)) {
return Model.sequenceName(model, suffix);
}
const tableName = isSequenceNameFn(Model.tableName)
? Model.tableName(model)
: (model?.name ?? "");
const anchor = suffix || "pk";
return [tableName, anchor].filter(Boolean).join("_");
}
function ensureSequenceOptions(
obj: any,
attr: any,
options: SequenceOptions
): void {
if (!options.type) {
const metaType = Metadata.type(obj.constructor, attr);
if (
![Number.name, String.name, BigInt.name].includes(
metaType?.name || metaType
)
)
throw new Error("Incorrrect option type");
options.type = metaType;
}
switch (options.type) {
case String.name || String.name.toLowerCase():
console.warn(`Deprecated "${options.type}" type in options`);
// eslint-disable-next-line no-fallthrough
case String:
options.generated =
typeof options.generated === "undefined" ? false : options.generated;
options.type = String;
break;
case Number.name || String.name.toLowerCase():
console.warn(`Deprecated "${options.type}" type in options`);
// eslint-disable-next-line no-fallthrough
case Number:
options.generated =
typeof options.generated === "undefined" ? true : options.generated;
options.type = Number;
break;
case BigInt.name || BigInt.name.toLowerCase():
console.warn(`Deprecated "${options.type}" type in options`);
// eslint-disable-next-line no-fallthrough
case BigInt:
options.type = BigInt;
options.generated =
typeof options.generated === "undefined" ? true : options.generated;
break;
case "uuid":
case "serial":
options.generated = true;
break;
default:
throw new Error("Unsupported type");
}
if (typeof options.generated === "undefined") {
options.generated = true;
}
}
/**
* @description Callback function for primary key creation
* @summary Handles the creation of primary key values for models using sequences
* @template M - Type that extends Model
* @template R - Type that extends Repo<M, F, C>
* @template V - Type that extends SequenceOptions
* @template F - Type that extends RepositoryFlags
* @template C - Type that extends Context<F>
* @param {Context<F>} context - The execution context
* @param {V} data - The sequence options
* @param key - The property key to set as primary key
* @param {M} model - The model instance
* @return {Promise<void>} A promise that resolves when the primary key is set
* @function pkOnCreate
* @category Property Decorators
* @mermaid
* sequenceDiagram
* participant Model
* participant pkOnCreate
* participant Adapter
* participant Sequence
*
* Model->>pkOnCreate: Call with model instance
* Note over pkOnCreate: Check if key already exists
* alt Key exists or no type specified
* pkOnCreate-->>Model: Return early
* else Key needs to be created
* pkOnCreate->>pkOnCreate: Generate sequence name if not provided
* pkOnCreate->>Adapter: Request Sequence(data)
* Adapter->>Sequence: Create sequence
* Sequence-->>pkOnCreate: Return sequence
* pkOnCreate->>Sequence: Call next()
* Sequence-->>pkOnCreate: Return next value
* pkOnCreate->>Model: Set primary key value
* end
*/
export async function pkOnCreate<
M extends Model,
R extends Repository<M, any>,
V extends SequenceOptions,
>(
this: R,
context: ContextOf<R>,
data: V,
key: keyof M,
model: M
): Promise<void> {
if (!data.type || !data.generated || model[key]) {
return;
}
const setPrimaryKeyValue = function <M extends Model>(
target: M,
propertyKey: string,
value: string | number | bigint
) {
Reflect.set(target, propertyKey, value);
};
if (!data.name) data.name = Model.sequenceName(model, "pk");
let sequence: Sequence;
try {
sequence = await this.adapter.Sequence(data, this._overrides);
} catch (e: any) {
throw new InternalError(
`Failed to instantiate Sequence ${data.name}: ${e}`
);
}
const next = await sequence.next(context);
setPrimaryKeyValue(model, key as string, next);
}
export async function sequenceOnCreateUpdate<
M extends Model,
R extends Repository<M, any>,
V extends SequenceOptions & SequenceDecoratorParams,
>(
this: R,
context: ContextOf<R>,
data: V,
key: keyof M,
model: M,
oldModel?: M
): Promise<void> {
if (!data.type || !data.generated) return;
if (!data.name) {
const id = Model.pk(model, true) as any;
if (typeof id === "undefined" || id === null) {
throw new InternalError(
`Cannot generate sequence without an id for ${model.constructor.name}`
);
}
data.name = Model.sequenceName(model, String(id), String(key));
}
let sequence: Sequence;
try {
sequence = await this.adapter.Sequence(data as any, this._overrides);
} catch (e: any) {
throw new InternalError(
`Failed to instantiate Sequence ${data.name}: ${e}`
);
}
const isUpdate = typeof oldModel !== "undefined" && oldModel !== null;
const hasValue = typeof model[key] !== "undefined" && model[key] !== null;
const allowGenerationOverride =
!!context.get("allowGenerationOverride") && hasValue;
// Always ensure the backing sequence exists. If a user-provided value exists
// and no sequence exists, use that value as the starting point.
if (hasValue) {
await (sequence as any).ensureAtLeast(model[key] as any, context);
}
// On update, only run if explicitly enabled; but if we did run, we still already
// ensured the sequence exists/seeded above.
if (isUpdate && !data.update) return;
// When generation override is enabled, keep the model's value but still ensure
// the backing sequence exists/has been seeded.
if (allowGenerationOverride) return;
const next = await sequence.next(context);
Reflect.set(model, key as string, next as any);
}
export function pkDec(options: SequenceOptions, groupsort?: GroupSort) {
return function pkDec(obj: any, attr: any) {
prop()(obj, attr);
ensureSequenceOptions(obj, attr, options);
if (!options.name) {
options.name = resolveSequenceName(obj.constructor, "pk");
}
const decs = [
propMetadata(Metadata.key(DBKeys.ID, attr), options),
propMetadata(Metadata.key(PersistenceKeys.SEQUENCE, attr), options),
index([OrderDirection.ASC, OrderDirection.DSC]),
required(),
readonly(),
onCreate(pkOnCreate, options, groupsort),
];
if (options.generated) decs.push(generated());
return apply(...decs)(obj, attr);
};
}
export function sequenceDec(
options: SequenceOptions,
params: SequenceDecoratorParams = {},
groupsort?: GroupSort
) {
return function sequenceDec(obj: any, attr: any) {
prop()(obj, attr);
ensureSequenceOptions(obj, attr, options);
const decs = [
required(),
propMetadata(Metadata.key(PersistenceKeys.SEQUENCE, attr), options),
onCreate(
sequenceOnCreateUpdate as any,
{ ...options, ...params } as any,
groupsort
),
];
if (params.update) {
decs.push(
onUpdate(
sequenceOnCreateUpdate as any,
{ ...options, ...params } as any,
groupsort
)
);
}
if (options.generated) decs.push(generated());
return apply(...decs)(obj, attr);
};
}
/**
* @description Primary Key Decorator
* @summary Marks a property as the model's primary key with automatic sequence generation
* This decorator combines multiple behaviors: it marks the property as unique, required,
* and ensures the index is created properly according to the provided sequence options.
* @param {Omit<SequenceOptions, "cycle" | "startWith" | "incrementBy">} opts - Options for the sequence generation
* @return {PropertyDecorator} A property decorator that can be applied to model properties
* @function pk
* @category Property Decorators
* @example
* ```typescript
* class User extends BaseModel {
* @pk()
* id!: string;
*
* @required()
* username!: string;
* }
* ```
*/
export function pk(
opts?: Partial<Omit<SequenceOptions, "cycle" | "startWith" | "incrementBy">>
) {
// We want to handle options.generated in the decorator function
const DefaultSequenceOptionsMin = Object.assign({}, DefaultSequenceOptions);
delete DefaultSequenceOptionsMin.generated;
opts = Object.assign({}, DefaultSequenceOptionsMin, opts) as SequenceOptions;
return Decoration.for(DBKeys.ID)
.define({
decorator: pkDec,
args: [opts, { priority: defaultPkPriority }],
})
.apply();
}
export function sequence(
opts?: Partial<Omit<SequenceOptions, "cycle" | "startWith" | "incrementBy">>,
updateOrParams?: SequenceDecoratorParams | boolean
) {
const DefaultSequenceOptionsMin = Object.assign({}, DefaultSequenceOptions);
delete DefaultSequenceOptionsMin.generated;
opts = Object.assign({}, DefaultSequenceOptionsMin, opts) as SequenceOptions;
const params: SequenceDecoratorParams =
typeof updateOrParams === "boolean"
? { update: updateOrParams }
: updateOrParams || {};
return Decoration.for(PersistenceKeys.SEQUENCE)
.define({
decorator: sequenceDec,
// Run after pk generation so the model id exists for per-instance sequences.
args: [opts, params, { priority: defaultPkPriority + 10 }],
})
.apply();
}
Source