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/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 and returns an array of values for a single column.
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,