metal-orm 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +481 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +480 -203
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/scripts/generate-entities/schema.mjs +196 -195
- package/src/decorators/decorator-metadata.ts +52 -46
- package/src/decorators/entity.ts +73 -68
- package/src/orm/entity-metadata.ts +301 -301
- package/src/orm/entity.ts +199 -199
- package/src/orm/save-graph.ts +446 -446
- package/src/orm/unit-of-work.ts +6 -6
- package/src/query-builder/select/cursor-pagination.ts +323 -0
- package/src/query-builder/select.ts +42 -1
- package/src/tree/tree-decorator.ts +137 -54
package/dist/index.cjs
CHANGED
|
@@ -395,6 +395,7 @@ __export(index_exports, {
|
|
|
395
395
|
sub: () => sub,
|
|
396
396
|
substr: () => substr,
|
|
397
397
|
sum: () => sum,
|
|
398
|
+
syncTreeEntityMetadata: () => syncTreeEntityMetadata,
|
|
398
399
|
synchronizeSchema: () => synchronizeSchema,
|
|
399
400
|
tableRef: () => tableRef,
|
|
400
401
|
tan: () => tan,
|
|
@@ -7713,6 +7714,193 @@ function buildWhereHasPredicate(env, context, relationFacet, createChildBuilder,
|
|
|
7713
7714
|
return negate ? notExists(finalSubAst) : exists(finalSubAst);
|
|
7714
7715
|
}
|
|
7715
7716
|
|
|
7717
|
+
// src/query-builder/select/cursor-pagination.ts
|
|
7718
|
+
function encodeCursor(payload) {
|
|
7719
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
7720
|
+
}
|
|
7721
|
+
function decodeCursor(cursor) {
|
|
7722
|
+
let parsed;
|
|
7723
|
+
try {
|
|
7724
|
+
parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
7725
|
+
} catch {
|
|
7726
|
+
throw new Error("executeCursor: invalid cursor format");
|
|
7727
|
+
}
|
|
7728
|
+
if (typeof parsed !== "object" || parsed === null || parsed.v !== 2 || !Array.isArray(parsed.values) || typeof parsed.orderSig !== "string") {
|
|
7729
|
+
throw new Error("executeCursor: invalid cursor payload");
|
|
7730
|
+
}
|
|
7731
|
+
return parsed;
|
|
7732
|
+
}
|
|
7733
|
+
function buildOrderSignature(specs) {
|
|
7734
|
+
return specs.map((s) => `${s.table}.${s.column}:${s.direction}`).join(",");
|
|
7735
|
+
}
|
|
7736
|
+
function extractOrderSpecs(ast) {
|
|
7737
|
+
if (!ast.orderBy || ast.orderBy.length === 0) {
|
|
7738
|
+
throw new Error("executeCursor: ORDER BY is required for cursor pagination");
|
|
7739
|
+
}
|
|
7740
|
+
return ast.orderBy.map((ob) => {
|
|
7741
|
+
if (ob.nulls) {
|
|
7742
|
+
throw new Error("executeCursor: NULLS FIRST/LAST is not supported for cursor pagination");
|
|
7743
|
+
}
|
|
7744
|
+
const term = ob.term;
|
|
7745
|
+
if (!term || term.type !== "Column") {
|
|
7746
|
+
throw new Error(
|
|
7747
|
+
"executeCursor: only column references are supported in ORDER BY for cursor pagination"
|
|
7748
|
+
);
|
|
7749
|
+
}
|
|
7750
|
+
const col2 = term;
|
|
7751
|
+
return {
|
|
7752
|
+
table: col2.table,
|
|
7753
|
+
column: col2.name,
|
|
7754
|
+
valueKey: resolveOrderValueKey(ast, col2),
|
|
7755
|
+
direction: ob.direction
|
|
7756
|
+
};
|
|
7757
|
+
});
|
|
7758
|
+
}
|
|
7759
|
+
function resolveOrderValueKey(ast, col2) {
|
|
7760
|
+
const projectedColumn = ast.columns.find(
|
|
7761
|
+
(candidate) => candidate.type === "Column" && candidate.table === col2.table && candidate.name === col2.name
|
|
7762
|
+
);
|
|
7763
|
+
return projectedColumn?.alias ?? projectedColumn?.name ?? col2.alias ?? col2.name;
|
|
7764
|
+
}
|
|
7765
|
+
function buildKeysetPredicate(specs, values, mode) {
|
|
7766
|
+
if (values.length !== specs.length) {
|
|
7767
|
+
throw new Error("executeCursor: invalid cursor payload");
|
|
7768
|
+
}
|
|
7769
|
+
const branches = [];
|
|
7770
|
+
for (let i = 0; i < specs.length; i++) {
|
|
7771
|
+
const spec = specs[i];
|
|
7772
|
+
const colNode = { type: "Column", table: spec.table, name: spec.column };
|
|
7773
|
+
const value = values[i];
|
|
7774
|
+
if (value === null || value === void 0) {
|
|
7775
|
+
throw new Error("executeCursor: invalid cursor payload");
|
|
7776
|
+
}
|
|
7777
|
+
const literal = { type: "Literal", value };
|
|
7778
|
+
let operator;
|
|
7779
|
+
if (mode === "after") {
|
|
7780
|
+
operator = spec.direction === "ASC" ? ">" : "<";
|
|
7781
|
+
} else {
|
|
7782
|
+
operator = spec.direction === "ASC" ? "<" : ">";
|
|
7783
|
+
}
|
|
7784
|
+
const eqParts = [];
|
|
7785
|
+
for (let j = 0; j < i; j++) {
|
|
7786
|
+
const prevSpec = specs[j];
|
|
7787
|
+
const prevCol = { type: "Column", table: prevSpec.table, name: prevSpec.column };
|
|
7788
|
+
const prevValue = values[j];
|
|
7789
|
+
if (prevValue === null || prevValue === void 0) {
|
|
7790
|
+
throw new Error("executeCursor: invalid cursor payload");
|
|
7791
|
+
}
|
|
7792
|
+
const prevVal = { type: "Literal", value: prevValue };
|
|
7793
|
+
eqParts.push({
|
|
7794
|
+
type: "BinaryExpression",
|
|
7795
|
+
left: prevCol,
|
|
7796
|
+
operator: "=",
|
|
7797
|
+
right: prevVal
|
|
7798
|
+
});
|
|
7799
|
+
}
|
|
7800
|
+
const breakExpr = {
|
|
7801
|
+
type: "BinaryExpression",
|
|
7802
|
+
left: colNode,
|
|
7803
|
+
operator,
|
|
7804
|
+
right: literal
|
|
7805
|
+
};
|
|
7806
|
+
if (eqParts.length === 0) {
|
|
7807
|
+
branches.push(breakExpr);
|
|
7808
|
+
} else {
|
|
7809
|
+
branches.push(and(...eqParts, breakExpr));
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
return branches.length === 1 ? branches[0] : or(...branches);
|
|
7813
|
+
}
|
|
7814
|
+
function buildCursorFromRow(row, specs) {
|
|
7815
|
+
const values = specs.map((spec) => {
|
|
7816
|
+
const value = row[spec.valueKey];
|
|
7817
|
+
if (value === null || value === void 0) {
|
|
7818
|
+
throw new Error("executeCursor: cursor pagination requires non-null ORDER BY values");
|
|
7819
|
+
}
|
|
7820
|
+
return value;
|
|
7821
|
+
});
|
|
7822
|
+
return encodeCursor({ v: 2, values, orderSig: buildOrderSignature(specs) });
|
|
7823
|
+
}
|
|
7824
|
+
function reverseDirection(direction) {
|
|
7825
|
+
return direction === "ASC" ? "DESC" : "ASC";
|
|
7826
|
+
}
|
|
7827
|
+
function createExecutionBuilder(builder, options) {
|
|
7828
|
+
const internals = builder.getInternals();
|
|
7829
|
+
const baseAst = internals.context.state.ast;
|
|
7830
|
+
const orderBy = options.reverseOrder && baseAst.orderBy ? baseAst.orderBy.map((order) => ({
|
|
7831
|
+
...order,
|
|
7832
|
+
direction: reverseDirection(order.direction)
|
|
7833
|
+
})) : baseAst.orderBy;
|
|
7834
|
+
const nextAst = {
|
|
7835
|
+
...baseAst,
|
|
7836
|
+
where: options.predicate ? baseAst.where ? and(baseAst.where, options.predicate) : options.predicate : baseAst.where,
|
|
7837
|
+
orderBy,
|
|
7838
|
+
limit: options.limit
|
|
7839
|
+
};
|
|
7840
|
+
const nextContext = {
|
|
7841
|
+
...internals.context,
|
|
7842
|
+
state: new SelectQueryState(builder.getTable(), nextAst)
|
|
7843
|
+
};
|
|
7844
|
+
return internals.clone(nextContext, internals.includeTree);
|
|
7845
|
+
}
|
|
7846
|
+
async function executeCursorQuery(builder, session, options) {
|
|
7847
|
+
const { first, after, last, before } = options;
|
|
7848
|
+
if (first != null && last != null) {
|
|
7849
|
+
throw new Error('executeCursor: "first" and "last" cannot be used together');
|
|
7850
|
+
}
|
|
7851
|
+
if (after != null && before != null) {
|
|
7852
|
+
throw new Error('executeCursor: "after" and "before" cannot be used together');
|
|
7853
|
+
}
|
|
7854
|
+
if (first == null && last == null) {
|
|
7855
|
+
throw new Error('executeCursor: either "first" or "last" must be provided');
|
|
7856
|
+
}
|
|
7857
|
+
const limit = first ?? last;
|
|
7858
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
7859
|
+
throw new Error(`executeCursor: "${first != null ? "first" : "last"}" must be an integer >= 1`);
|
|
7860
|
+
}
|
|
7861
|
+
const isBackward = last != null;
|
|
7862
|
+
const cursor = after ?? before;
|
|
7863
|
+
const ast = builder.getInternals().context.state.ast;
|
|
7864
|
+
const specs = extractOrderSpecs(ast);
|
|
7865
|
+
let predicate;
|
|
7866
|
+
if (cursor) {
|
|
7867
|
+
const decoded = decodeCursor(cursor);
|
|
7868
|
+
const expectedSig = buildOrderSignature(specs);
|
|
7869
|
+
if (decoded.orderSig !== expectedSig) {
|
|
7870
|
+
throw new Error(
|
|
7871
|
+
"executeCursor: cursor ORDER BY signature does not match the current query. The ORDER BY clause must remain the same between paginated requests."
|
|
7872
|
+
);
|
|
7873
|
+
}
|
|
7874
|
+
predicate = buildKeysetPredicate(specs, decoded.values, isBackward ? "before" : "after");
|
|
7875
|
+
}
|
|
7876
|
+
const executionBuilder = createExecutionBuilder(builder, {
|
|
7877
|
+
predicate,
|
|
7878
|
+
limit: limit + 1,
|
|
7879
|
+
reverseOrder: isBackward
|
|
7880
|
+
});
|
|
7881
|
+
const rows = await executionBuilder.execute(session);
|
|
7882
|
+
const hasExtraItem = rows.length > limit;
|
|
7883
|
+
if (hasExtraItem) {
|
|
7884
|
+
rows.pop();
|
|
7885
|
+
}
|
|
7886
|
+
const orderedRows = isBackward ? rows.reverse() : rows;
|
|
7887
|
+
const items = orderedRows;
|
|
7888
|
+
const hasItems = items.length > 0;
|
|
7889
|
+
const hasNextPage2 = hasItems ? isBackward ? before != null : hasExtraItem : false;
|
|
7890
|
+
const hasPreviousPage = hasItems ? isBackward ? hasExtraItem : after != null : false;
|
|
7891
|
+
const startCursor = hasItems ? buildCursorFromRow(items[0], specs) : null;
|
|
7892
|
+
const endCursor = hasItems ? buildCursorFromRow(items[items.length - 1], specs) : null;
|
|
7893
|
+
return {
|
|
7894
|
+
items,
|
|
7895
|
+
pageInfo: {
|
|
7896
|
+
hasNextPage: hasNextPage2,
|
|
7897
|
+
hasPreviousPage,
|
|
7898
|
+
startCursor,
|
|
7899
|
+
endCursor
|
|
7900
|
+
}
|
|
7901
|
+
};
|
|
7902
|
+
}
|
|
7903
|
+
|
|
7716
7904
|
// src/query-builder/select/from-facet.ts
|
|
7717
7905
|
var SelectFromFacet = class {
|
|
7718
7906
|
/**
|
|
@@ -8739,7 +8927,38 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
|
|
|
8739
8927
|
return executePagedQuery(builder, session, options, (sess) => builder.count(sess));
|
|
8740
8928
|
}
|
|
8741
8929
|
/**
|
|
8742
|
-
* Executes the query
|
|
8930
|
+
* Executes the query using cursor-based (keyset) pagination.
|
|
8931
|
+
* Requires a stable ORDER BY on selected, non-null columns.
|
|
8932
|
+
* Cursor pagination currently supports simple column references only and
|
|
8933
|
+
* the cursor token is opaque: it must be reused with the same ORDER BY signature.
|
|
8934
|
+
*
|
|
8935
|
+
* @param session - ORM session context
|
|
8936
|
+
* @param options - Cursor pagination options (`first`/`after` or `last`/`before`)
|
|
8937
|
+
* @returns Promise of cursor-paginated result with items and pageInfo
|
|
8938
|
+
* @example
|
|
8939
|
+
* const page1 = await selectFrom(users)
|
|
8940
|
+
* .orderBy(users.columns.createdAt, 'DESC')
|
|
8941
|
+
* .orderBy(users.columns.id, 'DESC')
|
|
8942
|
+
* .executeCursor(session, { first: 20 });
|
|
8943
|
+
*
|
|
8944
|
+
* // Next page
|
|
8945
|
+
* const page2 = await selectFrom(users)
|
|
8946
|
+
* .orderBy(users.columns.createdAt, 'DESC')
|
|
8947
|
+
* .orderBy(users.columns.id, 'DESC')
|
|
8948
|
+
* .executeCursor(session, { first: 20, after: page1.pageInfo.endCursor });
|
|
8949
|
+
*
|
|
8950
|
+
* // Previous page from a known cursor
|
|
8951
|
+
* const prevPage = await selectFrom(users)
|
|
8952
|
+
* .orderBy(users.columns.createdAt, 'DESC')
|
|
8953
|
+
* .orderBy(users.columns.id, 'DESC')
|
|
8954
|
+
* .executeCursor(session, { last: 20, before: page2.pageInfo.startCursor });
|
|
8955
|
+
*/
|
|
8956
|
+
async executeCursor(session, options) {
|
|
8957
|
+
const builder = this.ensureDefaultSelection();
|
|
8958
|
+
return executeCursorQuery(builder, session, options);
|
|
8959
|
+
}
|
|
8960
|
+
/**
|
|
8961
|
+
* Executes the query and returns an array of values for a single column.
|
|
8743
8962
|
* This is a convenience method to avoid manual `.map(r => r.column)`.
|
|
8744
8963
|
*
|
|
8745
8964
|
* @param column - The column name to extract
|
|
@@ -14969,6 +15188,265 @@ var jsonify = (value) => {
|
|
|
14969
15188
|
return result;
|
|
14970
15189
|
};
|
|
14971
15190
|
|
|
15191
|
+
// src/tree/tree-types.ts
|
|
15192
|
+
var DEFAULT_TREE_CONFIG = {
|
|
15193
|
+
parentKey: "parentId",
|
|
15194
|
+
leftKey: "lft",
|
|
15195
|
+
rightKey: "rght",
|
|
15196
|
+
cascadeCallbacks: false
|
|
15197
|
+
};
|
|
15198
|
+
function isTreeConfig(value) {
|
|
15199
|
+
if (typeof value !== "object" || value === null) return false;
|
|
15200
|
+
const obj = value;
|
|
15201
|
+
return typeof obj.parentKey === "string" && typeof obj.leftKey === "string" && typeof obj.rightKey === "string";
|
|
15202
|
+
}
|
|
15203
|
+
function resolveTreeConfig(partial) {
|
|
15204
|
+
return {
|
|
15205
|
+
parentKey: partial.parentKey ?? DEFAULT_TREE_CONFIG.parentKey,
|
|
15206
|
+
leftKey: partial.leftKey ?? DEFAULT_TREE_CONFIG.leftKey,
|
|
15207
|
+
rightKey: partial.rightKey ?? DEFAULT_TREE_CONFIG.rightKey,
|
|
15208
|
+
depthKey: partial.depthKey,
|
|
15209
|
+
scope: partial.scope,
|
|
15210
|
+
cascadeCallbacks: partial.cascadeCallbacks ?? DEFAULT_TREE_CONFIG.cascadeCallbacks,
|
|
15211
|
+
recoverOrder: partial.recoverOrder
|
|
15212
|
+
};
|
|
15213
|
+
}
|
|
15214
|
+
function validateTreeTable(table, config) {
|
|
15215
|
+
const missingColumns = [];
|
|
15216
|
+
const warnings = [];
|
|
15217
|
+
const requiredColumns = [config.parentKey, config.leftKey, config.rightKey];
|
|
15218
|
+
for (const col2 of requiredColumns) {
|
|
15219
|
+
if (!(col2 in table.columns)) {
|
|
15220
|
+
missingColumns.push(col2);
|
|
15221
|
+
}
|
|
15222
|
+
}
|
|
15223
|
+
if (config.depthKey && !(config.depthKey in table.columns)) {
|
|
15224
|
+
warnings.push(`Optional depth column '${config.depthKey}' not found in table '${table.name}'`);
|
|
15225
|
+
}
|
|
15226
|
+
if (config.scope) {
|
|
15227
|
+
for (const scopeCol of config.scope) {
|
|
15228
|
+
if (!(scopeCol in table.columns)) {
|
|
15229
|
+
missingColumns.push(scopeCol);
|
|
15230
|
+
}
|
|
15231
|
+
}
|
|
15232
|
+
}
|
|
15233
|
+
const leftCol = table.columns[config.leftKey];
|
|
15234
|
+
const rightCol = table.columns[config.rightKey];
|
|
15235
|
+
if (leftCol && leftCol.type !== "int" && leftCol.type !== "integer") {
|
|
15236
|
+
warnings.push(`Left column '${config.leftKey}' should be an integer type`);
|
|
15237
|
+
}
|
|
15238
|
+
if (rightCol && rightCol.type !== "int" && rightCol.type !== "integer") {
|
|
15239
|
+
warnings.push(`Right column '${config.rightKey}' should be an integer type`);
|
|
15240
|
+
}
|
|
15241
|
+
return {
|
|
15242
|
+
valid: missingColumns.length === 0,
|
|
15243
|
+
missingColumns,
|
|
15244
|
+
warnings
|
|
15245
|
+
};
|
|
15246
|
+
}
|
|
15247
|
+
function getTreeColumns(table, config) {
|
|
15248
|
+
const parentColumn = table.columns[config.parentKey];
|
|
15249
|
+
const leftColumn = table.columns[config.leftKey];
|
|
15250
|
+
const rightColumn = table.columns[config.rightKey];
|
|
15251
|
+
const depthColumn = config.depthKey ? table.columns[config.depthKey] : void 0;
|
|
15252
|
+
const scopeColumns = (config.scope ?? []).map((s) => table.columns[s]).filter(Boolean);
|
|
15253
|
+
if (!parentColumn || !leftColumn || !rightColumn) {
|
|
15254
|
+
throw new Error(
|
|
15255
|
+
`Table '${table.name}' is missing required tree columns. Required: ${config.parentKey}, ${config.leftKey}, ${config.rightKey}`
|
|
15256
|
+
);
|
|
15257
|
+
}
|
|
15258
|
+
return {
|
|
15259
|
+
parentColumn,
|
|
15260
|
+
leftColumn,
|
|
15261
|
+
rightColumn,
|
|
15262
|
+
depthColumn,
|
|
15263
|
+
scopeColumns
|
|
15264
|
+
};
|
|
15265
|
+
}
|
|
15266
|
+
|
|
15267
|
+
// src/tree/tree-decorator.ts
|
|
15268
|
+
var TREE_METADATA_KEY = /* @__PURE__ */ Symbol("metal-orm:tree");
|
|
15269
|
+
function Tree(options = {}) {
|
|
15270
|
+
return function(target, context) {
|
|
15271
|
+
const config = resolveTreeConfig(options);
|
|
15272
|
+
const metadataBag = readMetadataBag(context) ?? readMetadataBagFromConstructor(target);
|
|
15273
|
+
const metadata = {
|
|
15274
|
+
config,
|
|
15275
|
+
parentProperty: metadataBag?.tree?.parentProperty,
|
|
15276
|
+
childrenProperty: metadataBag?.tree?.childrenProperty
|
|
15277
|
+
};
|
|
15278
|
+
setTreeMetadataOnClass(target, metadata);
|
|
15279
|
+
registerTreeRelations(target, metadata);
|
|
15280
|
+
return target;
|
|
15281
|
+
};
|
|
15282
|
+
}
|
|
15283
|
+
function TreeParent() {
|
|
15284
|
+
return function(_value, context) {
|
|
15285
|
+
if (!context.name) {
|
|
15286
|
+
throw new Error("TreeParent decorator requires a property name");
|
|
15287
|
+
}
|
|
15288
|
+
if (context.private) {
|
|
15289
|
+
throw new Error("TreeParent decorator does not support private fields");
|
|
15290
|
+
}
|
|
15291
|
+
const propertyName = String(context.name);
|
|
15292
|
+
const bag = getOrCreateMetadataBag(context);
|
|
15293
|
+
bag.tree = { ...bag.tree, parentProperty: propertyName };
|
|
15294
|
+
};
|
|
15295
|
+
}
|
|
15296
|
+
function TreeChildren() {
|
|
15297
|
+
return function(_value, context) {
|
|
15298
|
+
if (!context.name) {
|
|
15299
|
+
throw new Error("TreeChildren decorator requires a property name");
|
|
15300
|
+
}
|
|
15301
|
+
if (context.private) {
|
|
15302
|
+
throw new Error("TreeChildren decorator does not support private fields");
|
|
15303
|
+
}
|
|
15304
|
+
const propertyName = String(context.name);
|
|
15305
|
+
const bag = getOrCreateMetadataBag(context);
|
|
15306
|
+
bag.tree = { ...bag.tree, childrenProperty: propertyName };
|
|
15307
|
+
};
|
|
15308
|
+
}
|
|
15309
|
+
function syncTreeEntityMetadata(ctor, treeMetadata) {
|
|
15310
|
+
const current = getTreeMetadata(ctor);
|
|
15311
|
+
if (!current) return;
|
|
15312
|
+
const metadataBag = readMetadataBagFromConstructor(ctor);
|
|
15313
|
+
const nextParentProperty = treeMetadata?.parentProperty ?? metadataBag?.tree?.parentProperty ?? current.parentProperty;
|
|
15314
|
+
const nextChildrenProperty = treeMetadata?.childrenProperty ?? metadataBag?.tree?.childrenProperty ?? current.childrenProperty;
|
|
15315
|
+
if (nextParentProperty !== current.parentProperty || nextChildrenProperty !== current.childrenProperty) {
|
|
15316
|
+
setTreeMetadata(ctor, {
|
|
15317
|
+
...current,
|
|
15318
|
+
parentProperty: nextParentProperty,
|
|
15319
|
+
childrenProperty: nextChildrenProperty
|
|
15320
|
+
});
|
|
15321
|
+
}
|
|
15322
|
+
registerTreeRelations(ctor, {
|
|
15323
|
+
...current,
|
|
15324
|
+
parentProperty: nextParentProperty,
|
|
15325
|
+
childrenProperty: nextChildrenProperty
|
|
15326
|
+
});
|
|
15327
|
+
}
|
|
15328
|
+
function getTreeMetadata(target) {
|
|
15329
|
+
return target[TREE_METADATA_KEY];
|
|
15330
|
+
}
|
|
15331
|
+
function setTreeMetadata(target, metadata) {
|
|
15332
|
+
target[TREE_METADATA_KEY] = metadata;
|
|
15333
|
+
}
|
|
15334
|
+
var registerTreeRelations = (target, metadata) => {
|
|
15335
|
+
const entityMeta = getEntityMetadata(target);
|
|
15336
|
+
if (!entityMeta) return;
|
|
15337
|
+
if (metadata.parentProperty && !entityMeta.relations[metadata.parentProperty]) {
|
|
15338
|
+
addRelationMetadata(target, metadata.parentProperty, {
|
|
15339
|
+
kind: RelationKinds.BelongsTo,
|
|
15340
|
+
propertyKey: metadata.parentProperty,
|
|
15341
|
+
target: () => target,
|
|
15342
|
+
foreignKey: metadata.config.parentKey
|
|
15343
|
+
});
|
|
15344
|
+
if (entityMeta.table && !entityMeta.table.relations[metadata.parentProperty]) {
|
|
15345
|
+
entityMeta.table.relations[metadata.parentProperty] = belongsTo(
|
|
15346
|
+
entityMeta.table,
|
|
15347
|
+
metadata.config.parentKey
|
|
15348
|
+
);
|
|
15349
|
+
}
|
|
15350
|
+
}
|
|
15351
|
+
if (metadata.childrenProperty && !entityMeta.relations[metadata.childrenProperty]) {
|
|
15352
|
+
addRelationMetadata(target, metadata.childrenProperty, {
|
|
15353
|
+
kind: RelationKinds.HasMany,
|
|
15354
|
+
propertyKey: metadata.childrenProperty,
|
|
15355
|
+
target: () => target,
|
|
15356
|
+
foreignKey: metadata.config.parentKey
|
|
15357
|
+
});
|
|
15358
|
+
if (entityMeta.table && !entityMeta.table.relations[metadata.childrenProperty]) {
|
|
15359
|
+
entityMeta.table.relations[metadata.childrenProperty] = hasMany(
|
|
15360
|
+
entityMeta.table,
|
|
15361
|
+
metadata.config.parentKey
|
|
15362
|
+
);
|
|
15363
|
+
}
|
|
15364
|
+
}
|
|
15365
|
+
};
|
|
15366
|
+
function setTreeMetadataOnClass(target, metadata) {
|
|
15367
|
+
target[TREE_METADATA_KEY] = metadata;
|
|
15368
|
+
}
|
|
15369
|
+
function hasTreeBehavior(target) {
|
|
15370
|
+
return getTreeMetadata(target) !== void 0;
|
|
15371
|
+
}
|
|
15372
|
+
function getTreeConfig(target) {
|
|
15373
|
+
return getTreeMetadata(target)?.config;
|
|
15374
|
+
}
|
|
15375
|
+
var TreeEntityRegistry = class {
|
|
15376
|
+
entities = /* @__PURE__ */ new Map();
|
|
15377
|
+
tableNames = /* @__PURE__ */ new Map();
|
|
15378
|
+
/**
|
|
15379
|
+
* Registers an entity class with its table name.
|
|
15380
|
+
*/
|
|
15381
|
+
register(entityClass, tableName) {
|
|
15382
|
+
this.entities.set(tableName, entityClass);
|
|
15383
|
+
this.tableNames.set(entityClass, tableName);
|
|
15384
|
+
}
|
|
15385
|
+
/**
|
|
15386
|
+
* Gets the entity class for a table name.
|
|
15387
|
+
*/
|
|
15388
|
+
getByTableName(tableName) {
|
|
15389
|
+
return this.entities.get(tableName);
|
|
15390
|
+
}
|
|
15391
|
+
/**
|
|
15392
|
+
* Gets the table name for an entity class.
|
|
15393
|
+
*/
|
|
15394
|
+
getTableName(entityClass) {
|
|
15395
|
+
return this.tableNames.get(entityClass);
|
|
15396
|
+
}
|
|
15397
|
+
/**
|
|
15398
|
+
* Checks if a table has tree behavior.
|
|
15399
|
+
*/
|
|
15400
|
+
isTreeTable(tableName) {
|
|
15401
|
+
const entity = this.entities.get(tableName);
|
|
15402
|
+
return entity ? hasTreeBehavior(entity) : false;
|
|
15403
|
+
}
|
|
15404
|
+
/**
|
|
15405
|
+
* Gets all registered tree entities.
|
|
15406
|
+
*/
|
|
15407
|
+
getAll() {
|
|
15408
|
+
const result = [];
|
|
15409
|
+
for (const [tableName, entityClass] of this.entities) {
|
|
15410
|
+
const metadata = getTreeMetadata(entityClass);
|
|
15411
|
+
if (metadata) {
|
|
15412
|
+
result.push({ entityClass, tableName, metadata });
|
|
15413
|
+
}
|
|
15414
|
+
}
|
|
15415
|
+
return result;
|
|
15416
|
+
}
|
|
15417
|
+
/**
|
|
15418
|
+
* Clears the registry.
|
|
15419
|
+
*/
|
|
15420
|
+
clear() {
|
|
15421
|
+
this.entities.clear();
|
|
15422
|
+
this.tableNames.clear();
|
|
15423
|
+
}
|
|
15424
|
+
};
|
|
15425
|
+
var treeEntityRegistry = new TreeEntityRegistry();
|
|
15426
|
+
function getTreeBounds(entity, config) {
|
|
15427
|
+
const data = entity;
|
|
15428
|
+
const lft = data[config.leftKey];
|
|
15429
|
+
const rght = data[config.rightKey];
|
|
15430
|
+
if (typeof lft !== "number" || typeof rght !== "number") {
|
|
15431
|
+
return null;
|
|
15432
|
+
}
|
|
15433
|
+
return { lft, rght };
|
|
15434
|
+
}
|
|
15435
|
+
function getTreeParentId(entity, config) {
|
|
15436
|
+
return entity[config.parentKey];
|
|
15437
|
+
}
|
|
15438
|
+
function setTreeBounds(entity, config, lft, rght, depth) {
|
|
15439
|
+
const data = entity;
|
|
15440
|
+
data[config.leftKey] = lft;
|
|
15441
|
+
data[config.rightKey] = rght;
|
|
15442
|
+
if (config.depthKey && depth !== void 0) {
|
|
15443
|
+
data[config.depthKey] = depth;
|
|
15444
|
+
}
|
|
15445
|
+
}
|
|
15446
|
+
function setTreeParentId(entity, config, parentId) {
|
|
15447
|
+
entity[config.parentKey] = parentId;
|
|
15448
|
+
}
|
|
15449
|
+
|
|
14972
15450
|
// src/decorators/entity.ts
|
|
14973
15451
|
var toSnakeCase2 = (value) => {
|
|
14974
15452
|
return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9_]+/gi, "_").replace(/__+/g, "_").replace(/^_|_$/g, "").toLowerCase();
|
|
@@ -15012,6 +15490,7 @@ function Entity(options = {}) {
|
|
|
15012
15490
|
addRelationMetadata(ctor, entry.propertyName, relationCopy);
|
|
15013
15491
|
}
|
|
15014
15492
|
}
|
|
15493
|
+
syncTreeEntityMetadata(ctor, bag?.tree);
|
|
15015
15494
|
return ctor;
|
|
15016
15495
|
};
|
|
15017
15496
|
}
|
|
@@ -17938,208 +18417,6 @@ function extractReusableSchemas(schema, existing = {}, prefix = "") {
|
|
|
17938
18417
|
return existing;
|
|
17939
18418
|
}
|
|
17940
18419
|
|
|
17941
|
-
// src/tree/tree-types.ts
|
|
17942
|
-
var DEFAULT_TREE_CONFIG = {
|
|
17943
|
-
parentKey: "parentId",
|
|
17944
|
-
leftKey: "lft",
|
|
17945
|
-
rightKey: "rght",
|
|
17946
|
-
cascadeCallbacks: false
|
|
17947
|
-
};
|
|
17948
|
-
function isTreeConfig(value) {
|
|
17949
|
-
if (typeof value !== "object" || value === null) return false;
|
|
17950
|
-
const obj = value;
|
|
17951
|
-
return typeof obj.parentKey === "string" && typeof obj.leftKey === "string" && typeof obj.rightKey === "string";
|
|
17952
|
-
}
|
|
17953
|
-
function resolveTreeConfig(partial) {
|
|
17954
|
-
return {
|
|
17955
|
-
parentKey: partial.parentKey ?? DEFAULT_TREE_CONFIG.parentKey,
|
|
17956
|
-
leftKey: partial.leftKey ?? DEFAULT_TREE_CONFIG.leftKey,
|
|
17957
|
-
rightKey: partial.rightKey ?? DEFAULT_TREE_CONFIG.rightKey,
|
|
17958
|
-
depthKey: partial.depthKey,
|
|
17959
|
-
scope: partial.scope,
|
|
17960
|
-
cascadeCallbacks: partial.cascadeCallbacks ?? DEFAULT_TREE_CONFIG.cascadeCallbacks,
|
|
17961
|
-
recoverOrder: partial.recoverOrder
|
|
17962
|
-
};
|
|
17963
|
-
}
|
|
17964
|
-
function validateTreeTable(table, config) {
|
|
17965
|
-
const missingColumns = [];
|
|
17966
|
-
const warnings = [];
|
|
17967
|
-
const requiredColumns = [config.parentKey, config.leftKey, config.rightKey];
|
|
17968
|
-
for (const col2 of requiredColumns) {
|
|
17969
|
-
if (!(col2 in table.columns)) {
|
|
17970
|
-
missingColumns.push(col2);
|
|
17971
|
-
}
|
|
17972
|
-
}
|
|
17973
|
-
if (config.depthKey && !(config.depthKey in table.columns)) {
|
|
17974
|
-
warnings.push(`Optional depth column '${config.depthKey}' not found in table '${table.name}'`);
|
|
17975
|
-
}
|
|
17976
|
-
if (config.scope) {
|
|
17977
|
-
for (const scopeCol of config.scope) {
|
|
17978
|
-
if (!(scopeCol in table.columns)) {
|
|
17979
|
-
missingColumns.push(scopeCol);
|
|
17980
|
-
}
|
|
17981
|
-
}
|
|
17982
|
-
}
|
|
17983
|
-
const leftCol = table.columns[config.leftKey];
|
|
17984
|
-
const rightCol = table.columns[config.rightKey];
|
|
17985
|
-
if (leftCol && leftCol.type !== "int" && leftCol.type !== "integer") {
|
|
17986
|
-
warnings.push(`Left column '${config.leftKey}' should be an integer type`);
|
|
17987
|
-
}
|
|
17988
|
-
if (rightCol && rightCol.type !== "int" && rightCol.type !== "integer") {
|
|
17989
|
-
warnings.push(`Right column '${config.rightKey}' should be an integer type`);
|
|
17990
|
-
}
|
|
17991
|
-
return {
|
|
17992
|
-
valid: missingColumns.length === 0,
|
|
17993
|
-
missingColumns,
|
|
17994
|
-
warnings
|
|
17995
|
-
};
|
|
17996
|
-
}
|
|
17997
|
-
function getTreeColumns(table, config) {
|
|
17998
|
-
const parentColumn = table.columns[config.parentKey];
|
|
17999
|
-
const leftColumn = table.columns[config.leftKey];
|
|
18000
|
-
const rightColumn = table.columns[config.rightKey];
|
|
18001
|
-
const depthColumn = config.depthKey ? table.columns[config.depthKey] : void 0;
|
|
18002
|
-
const scopeColumns = (config.scope ?? []).map((s) => table.columns[s]).filter(Boolean);
|
|
18003
|
-
if (!parentColumn || !leftColumn || !rightColumn) {
|
|
18004
|
-
throw new Error(
|
|
18005
|
-
`Table '${table.name}' is missing required tree columns. Required: ${config.parentKey}, ${config.leftKey}, ${config.rightKey}`
|
|
18006
|
-
);
|
|
18007
|
-
}
|
|
18008
|
-
return {
|
|
18009
|
-
parentColumn,
|
|
18010
|
-
leftColumn,
|
|
18011
|
-
rightColumn,
|
|
18012
|
-
depthColumn,
|
|
18013
|
-
scopeColumns
|
|
18014
|
-
};
|
|
18015
|
-
}
|
|
18016
|
-
|
|
18017
|
-
// src/tree/tree-decorator.ts
|
|
18018
|
-
var TREE_METADATA_KEY = /* @__PURE__ */ Symbol("metal-orm:tree");
|
|
18019
|
-
function Tree(options = {}) {
|
|
18020
|
-
return function(target) {
|
|
18021
|
-
const config = resolveTreeConfig(options);
|
|
18022
|
-
const metadata = { config };
|
|
18023
|
-
setTreeMetadataOnClass(target, metadata);
|
|
18024
|
-
return target;
|
|
18025
|
-
};
|
|
18026
|
-
}
|
|
18027
|
-
function TreeParent() {
|
|
18028
|
-
return function(_value, context) {
|
|
18029
|
-
const propertyName = String(context.name);
|
|
18030
|
-
context.addInitializer(function() {
|
|
18031
|
-
const constructor = this.constructor;
|
|
18032
|
-
const metadata = getTreeMetadata(constructor);
|
|
18033
|
-
if (metadata) {
|
|
18034
|
-
metadata.parentProperty = propertyName;
|
|
18035
|
-
setTreeMetadata(constructor, metadata);
|
|
18036
|
-
}
|
|
18037
|
-
});
|
|
18038
|
-
};
|
|
18039
|
-
}
|
|
18040
|
-
function TreeChildren() {
|
|
18041
|
-
return function(_value, context) {
|
|
18042
|
-
const propertyName = String(context.name);
|
|
18043
|
-
context.addInitializer(function() {
|
|
18044
|
-
const constructor = this.constructor;
|
|
18045
|
-
const metadata = getTreeMetadata(constructor);
|
|
18046
|
-
if (metadata) {
|
|
18047
|
-
metadata.childrenProperty = propertyName;
|
|
18048
|
-
setTreeMetadata(constructor, metadata);
|
|
18049
|
-
}
|
|
18050
|
-
});
|
|
18051
|
-
};
|
|
18052
|
-
}
|
|
18053
|
-
function getTreeMetadata(target) {
|
|
18054
|
-
return target[TREE_METADATA_KEY];
|
|
18055
|
-
}
|
|
18056
|
-
function setTreeMetadata(target, metadata) {
|
|
18057
|
-
target[TREE_METADATA_KEY] = metadata;
|
|
18058
|
-
}
|
|
18059
|
-
function setTreeMetadataOnClass(target, metadata) {
|
|
18060
|
-
target[TREE_METADATA_KEY] = metadata;
|
|
18061
|
-
}
|
|
18062
|
-
function hasTreeBehavior(target) {
|
|
18063
|
-
return getTreeMetadata(target) !== void 0;
|
|
18064
|
-
}
|
|
18065
|
-
function getTreeConfig(target) {
|
|
18066
|
-
return getTreeMetadata(target)?.config;
|
|
18067
|
-
}
|
|
18068
|
-
var TreeEntityRegistry = class {
|
|
18069
|
-
entities = /* @__PURE__ */ new Map();
|
|
18070
|
-
tableNames = /* @__PURE__ */ new Map();
|
|
18071
|
-
/**
|
|
18072
|
-
* Registers an entity class with its table name.
|
|
18073
|
-
*/
|
|
18074
|
-
register(entityClass, tableName) {
|
|
18075
|
-
this.entities.set(tableName, entityClass);
|
|
18076
|
-
this.tableNames.set(entityClass, tableName);
|
|
18077
|
-
}
|
|
18078
|
-
/**
|
|
18079
|
-
* Gets the entity class for a table name.
|
|
18080
|
-
*/
|
|
18081
|
-
getByTableName(tableName) {
|
|
18082
|
-
return this.entities.get(tableName);
|
|
18083
|
-
}
|
|
18084
|
-
/**
|
|
18085
|
-
* Gets the table name for an entity class.
|
|
18086
|
-
*/
|
|
18087
|
-
getTableName(entityClass) {
|
|
18088
|
-
return this.tableNames.get(entityClass);
|
|
18089
|
-
}
|
|
18090
|
-
/**
|
|
18091
|
-
* Checks if a table has tree behavior.
|
|
18092
|
-
*/
|
|
18093
|
-
isTreeTable(tableName) {
|
|
18094
|
-
const entity = this.entities.get(tableName);
|
|
18095
|
-
return entity ? hasTreeBehavior(entity) : false;
|
|
18096
|
-
}
|
|
18097
|
-
/**
|
|
18098
|
-
* Gets all registered tree entities.
|
|
18099
|
-
*/
|
|
18100
|
-
getAll() {
|
|
18101
|
-
const result = [];
|
|
18102
|
-
for (const [tableName, entityClass] of this.entities) {
|
|
18103
|
-
const metadata = getTreeMetadata(entityClass);
|
|
18104
|
-
if (metadata) {
|
|
18105
|
-
result.push({ entityClass, tableName, metadata });
|
|
18106
|
-
}
|
|
18107
|
-
}
|
|
18108
|
-
return result;
|
|
18109
|
-
}
|
|
18110
|
-
/**
|
|
18111
|
-
* Clears the registry.
|
|
18112
|
-
*/
|
|
18113
|
-
clear() {
|
|
18114
|
-
this.entities.clear();
|
|
18115
|
-
this.tableNames.clear();
|
|
18116
|
-
}
|
|
18117
|
-
};
|
|
18118
|
-
var treeEntityRegistry = new TreeEntityRegistry();
|
|
18119
|
-
function getTreeBounds(entity, config) {
|
|
18120
|
-
const data = entity;
|
|
18121
|
-
const lft = data[config.leftKey];
|
|
18122
|
-
const rght = data[config.rightKey];
|
|
18123
|
-
if (typeof lft !== "number" || typeof rght !== "number") {
|
|
18124
|
-
return null;
|
|
18125
|
-
}
|
|
18126
|
-
return { lft, rght };
|
|
18127
|
-
}
|
|
18128
|
-
function getTreeParentId(entity, config) {
|
|
18129
|
-
return entity[config.parentKey];
|
|
18130
|
-
}
|
|
18131
|
-
function setTreeBounds(entity, config, lft, rght, depth) {
|
|
18132
|
-
const data = entity;
|
|
18133
|
-
data[config.leftKey] = lft;
|
|
18134
|
-
data[config.rightKey] = rght;
|
|
18135
|
-
if (config.depthKey && depth !== void 0) {
|
|
18136
|
-
data[config.depthKey] = depth;
|
|
18137
|
-
}
|
|
18138
|
-
}
|
|
18139
|
-
function setTreeParentId(entity, config, parentId) {
|
|
18140
|
-
entity[config.parentKey] = parentId;
|
|
18141
|
-
}
|
|
18142
|
-
|
|
18143
18420
|
// src/dto/openapi/generators/tree.ts
|
|
18144
18421
|
function threadedNodeToOpenApiSchema(target, options) {
|
|
18145
18422
|
const componentName = options?.componentName;
|
|
@@ -20164,6 +20441,7 @@ var TagIndex = class {
|
|
|
20164
20441
|
sub,
|
|
20165
20442
|
substr,
|
|
20166
20443
|
sum,
|
|
20444
|
+
syncTreeEntityMetadata,
|
|
20167
20445
|
synchronizeSchema,
|
|
20168
20446
|
tableRef,
|
|
20169
20447
|
tan,
|