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.js CHANGED
@@ -7310,6 +7310,193 @@ function buildWhereHasPredicate(env, context, relationFacet, createChildBuilder,
7310
7310
  return negate ? notExists(finalSubAst) : exists(finalSubAst);
7311
7311
  }
7312
7312
 
7313
+ // src/query-builder/select/cursor-pagination.ts
7314
+ function encodeCursor(payload) {
7315
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
7316
+ }
7317
+ function decodeCursor(cursor) {
7318
+ let parsed;
7319
+ try {
7320
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
7321
+ } catch {
7322
+ throw new Error("executeCursor: invalid cursor format");
7323
+ }
7324
+ if (typeof parsed !== "object" || parsed === null || parsed.v !== 2 || !Array.isArray(parsed.values) || typeof parsed.orderSig !== "string") {
7325
+ throw new Error("executeCursor: invalid cursor payload");
7326
+ }
7327
+ return parsed;
7328
+ }
7329
+ function buildOrderSignature(specs) {
7330
+ return specs.map((s) => `${s.table}.${s.column}:${s.direction}`).join(",");
7331
+ }
7332
+ function extractOrderSpecs(ast) {
7333
+ if (!ast.orderBy || ast.orderBy.length === 0) {
7334
+ throw new Error("executeCursor: ORDER BY is required for cursor pagination");
7335
+ }
7336
+ return ast.orderBy.map((ob) => {
7337
+ if (ob.nulls) {
7338
+ throw new Error("executeCursor: NULLS FIRST/LAST is not supported for cursor pagination");
7339
+ }
7340
+ const term = ob.term;
7341
+ if (!term || term.type !== "Column") {
7342
+ throw new Error(
7343
+ "executeCursor: only column references are supported in ORDER BY for cursor pagination"
7344
+ );
7345
+ }
7346
+ const col2 = term;
7347
+ return {
7348
+ table: col2.table,
7349
+ column: col2.name,
7350
+ valueKey: resolveOrderValueKey(ast, col2),
7351
+ direction: ob.direction
7352
+ };
7353
+ });
7354
+ }
7355
+ function resolveOrderValueKey(ast, col2) {
7356
+ const projectedColumn = ast.columns.find(
7357
+ (candidate) => candidate.type === "Column" && candidate.table === col2.table && candidate.name === col2.name
7358
+ );
7359
+ return projectedColumn?.alias ?? projectedColumn?.name ?? col2.alias ?? col2.name;
7360
+ }
7361
+ function buildKeysetPredicate(specs, values, mode) {
7362
+ if (values.length !== specs.length) {
7363
+ throw new Error("executeCursor: invalid cursor payload");
7364
+ }
7365
+ const branches = [];
7366
+ for (let i = 0; i < specs.length; i++) {
7367
+ const spec = specs[i];
7368
+ const colNode = { type: "Column", table: spec.table, name: spec.column };
7369
+ const value = values[i];
7370
+ if (value === null || value === void 0) {
7371
+ throw new Error("executeCursor: invalid cursor payload");
7372
+ }
7373
+ const literal = { type: "Literal", value };
7374
+ let operator;
7375
+ if (mode === "after") {
7376
+ operator = spec.direction === "ASC" ? ">" : "<";
7377
+ } else {
7378
+ operator = spec.direction === "ASC" ? "<" : ">";
7379
+ }
7380
+ const eqParts = [];
7381
+ for (let j = 0; j < i; j++) {
7382
+ const prevSpec = specs[j];
7383
+ const prevCol = { type: "Column", table: prevSpec.table, name: prevSpec.column };
7384
+ const prevValue = values[j];
7385
+ if (prevValue === null || prevValue === void 0) {
7386
+ throw new Error("executeCursor: invalid cursor payload");
7387
+ }
7388
+ const prevVal = { type: "Literal", value: prevValue };
7389
+ eqParts.push({
7390
+ type: "BinaryExpression",
7391
+ left: prevCol,
7392
+ operator: "=",
7393
+ right: prevVal
7394
+ });
7395
+ }
7396
+ const breakExpr = {
7397
+ type: "BinaryExpression",
7398
+ left: colNode,
7399
+ operator,
7400
+ right: literal
7401
+ };
7402
+ if (eqParts.length === 0) {
7403
+ branches.push(breakExpr);
7404
+ } else {
7405
+ branches.push(and(...eqParts, breakExpr));
7406
+ }
7407
+ }
7408
+ return branches.length === 1 ? branches[0] : or(...branches);
7409
+ }
7410
+ function buildCursorFromRow(row, specs) {
7411
+ const values = specs.map((spec) => {
7412
+ const value = row[spec.valueKey];
7413
+ if (value === null || value === void 0) {
7414
+ throw new Error("executeCursor: cursor pagination requires non-null ORDER BY values");
7415
+ }
7416
+ return value;
7417
+ });
7418
+ return encodeCursor({ v: 2, values, orderSig: buildOrderSignature(specs) });
7419
+ }
7420
+ function reverseDirection(direction) {
7421
+ return direction === "ASC" ? "DESC" : "ASC";
7422
+ }
7423
+ function createExecutionBuilder(builder, options) {
7424
+ const internals = builder.getInternals();
7425
+ const baseAst = internals.context.state.ast;
7426
+ const orderBy = options.reverseOrder && baseAst.orderBy ? baseAst.orderBy.map((order) => ({
7427
+ ...order,
7428
+ direction: reverseDirection(order.direction)
7429
+ })) : baseAst.orderBy;
7430
+ const nextAst = {
7431
+ ...baseAst,
7432
+ where: options.predicate ? baseAst.where ? and(baseAst.where, options.predicate) : options.predicate : baseAst.where,
7433
+ orderBy,
7434
+ limit: options.limit
7435
+ };
7436
+ const nextContext = {
7437
+ ...internals.context,
7438
+ state: new SelectQueryState(builder.getTable(), nextAst)
7439
+ };
7440
+ return internals.clone(nextContext, internals.includeTree);
7441
+ }
7442
+ async function executeCursorQuery(builder, session, options) {
7443
+ const { first, after, last, before } = options;
7444
+ if (first != null && last != null) {
7445
+ throw new Error('executeCursor: "first" and "last" cannot be used together');
7446
+ }
7447
+ if (after != null && before != null) {
7448
+ throw new Error('executeCursor: "after" and "before" cannot be used together');
7449
+ }
7450
+ if (first == null && last == null) {
7451
+ throw new Error('executeCursor: either "first" or "last" must be provided');
7452
+ }
7453
+ const limit = first ?? last;
7454
+ if (!Number.isInteger(limit) || limit < 1) {
7455
+ throw new Error(`executeCursor: "${first != null ? "first" : "last"}" must be an integer >= 1`);
7456
+ }
7457
+ const isBackward = last != null;
7458
+ const cursor = after ?? before;
7459
+ const ast = builder.getInternals().context.state.ast;
7460
+ const specs = extractOrderSpecs(ast);
7461
+ let predicate;
7462
+ if (cursor) {
7463
+ const decoded = decodeCursor(cursor);
7464
+ const expectedSig = buildOrderSignature(specs);
7465
+ if (decoded.orderSig !== expectedSig) {
7466
+ throw new Error(
7467
+ "executeCursor: cursor ORDER BY signature does not match the current query. The ORDER BY clause must remain the same between paginated requests."
7468
+ );
7469
+ }
7470
+ predicate = buildKeysetPredicate(specs, decoded.values, isBackward ? "before" : "after");
7471
+ }
7472
+ const executionBuilder = createExecutionBuilder(builder, {
7473
+ predicate,
7474
+ limit: limit + 1,
7475
+ reverseOrder: isBackward
7476
+ });
7477
+ const rows = await executionBuilder.execute(session);
7478
+ const hasExtraItem = rows.length > limit;
7479
+ if (hasExtraItem) {
7480
+ rows.pop();
7481
+ }
7482
+ const orderedRows = isBackward ? rows.reverse() : rows;
7483
+ const items = orderedRows;
7484
+ const hasItems = items.length > 0;
7485
+ const hasNextPage2 = hasItems ? isBackward ? before != null : hasExtraItem : false;
7486
+ const hasPreviousPage = hasItems ? isBackward ? hasExtraItem : after != null : false;
7487
+ const startCursor = hasItems ? buildCursorFromRow(items[0], specs) : null;
7488
+ const endCursor = hasItems ? buildCursorFromRow(items[items.length - 1], specs) : null;
7489
+ return {
7490
+ items,
7491
+ pageInfo: {
7492
+ hasNextPage: hasNextPage2,
7493
+ hasPreviousPage,
7494
+ startCursor,
7495
+ endCursor
7496
+ }
7497
+ };
7498
+ }
7499
+
7313
7500
  // src/query-builder/select/from-facet.ts
7314
7501
  var SelectFromFacet = class {
7315
7502
  /**
@@ -8336,7 +8523,38 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
8336
8523
  return executePagedQuery(builder, session, options, (sess) => builder.count(sess));
8337
8524
  }
8338
8525
  /**
8339
- * Executes the query and returns an array of values for a single column.
8526
+ * Executes the query using cursor-based (keyset) pagination.
8527
+ * Requires a stable ORDER BY on selected, non-null columns.
8528
+ * Cursor pagination currently supports simple column references only and
8529
+ * the cursor token is opaque: it must be reused with the same ORDER BY signature.
8530
+ *
8531
+ * @param session - ORM session context
8532
+ * @param options - Cursor pagination options (`first`/`after` or `last`/`before`)
8533
+ * @returns Promise of cursor-paginated result with items and pageInfo
8534
+ * @example
8535
+ * const page1 = await selectFrom(users)
8536
+ * .orderBy(users.columns.createdAt, 'DESC')
8537
+ * .orderBy(users.columns.id, 'DESC')
8538
+ * .executeCursor(session, { first: 20 });
8539
+ *
8540
+ * // Next page
8541
+ * const page2 = await selectFrom(users)
8542
+ * .orderBy(users.columns.createdAt, 'DESC')
8543
+ * .orderBy(users.columns.id, 'DESC')
8544
+ * .executeCursor(session, { first: 20, after: page1.pageInfo.endCursor });
8545
+ *
8546
+ * // Previous page from a known cursor
8547
+ * const prevPage = await selectFrom(users)
8548
+ * .orderBy(users.columns.createdAt, 'DESC')
8549
+ * .orderBy(users.columns.id, 'DESC')
8550
+ * .executeCursor(session, { last: 20, before: page2.pageInfo.startCursor });
8551
+ */
8552
+ async executeCursor(session, options) {
8553
+ const builder = this.ensureDefaultSelection();
8554
+ return executeCursorQuery(builder, session, options);
8555
+ }
8556
+ /**
8557
+ * Executes the query and returns an array of values for a single column.
8340
8558
  * This is a convenience method to avoid manual `.map(r => r.column)`.
8341
8559
  *
8342
8560
  * @param column - The column name to extract
@@ -14566,6 +14784,265 @@ var jsonify = (value) => {
14566
14784
  return result;
14567
14785
  };
14568
14786
 
14787
+ // src/tree/tree-types.ts
14788
+ var DEFAULT_TREE_CONFIG = {
14789
+ parentKey: "parentId",
14790
+ leftKey: "lft",
14791
+ rightKey: "rght",
14792
+ cascadeCallbacks: false
14793
+ };
14794
+ function isTreeConfig(value) {
14795
+ if (typeof value !== "object" || value === null) return false;
14796
+ const obj = value;
14797
+ return typeof obj.parentKey === "string" && typeof obj.leftKey === "string" && typeof obj.rightKey === "string";
14798
+ }
14799
+ function resolveTreeConfig(partial) {
14800
+ return {
14801
+ parentKey: partial.parentKey ?? DEFAULT_TREE_CONFIG.parentKey,
14802
+ leftKey: partial.leftKey ?? DEFAULT_TREE_CONFIG.leftKey,
14803
+ rightKey: partial.rightKey ?? DEFAULT_TREE_CONFIG.rightKey,
14804
+ depthKey: partial.depthKey,
14805
+ scope: partial.scope,
14806
+ cascadeCallbacks: partial.cascadeCallbacks ?? DEFAULT_TREE_CONFIG.cascadeCallbacks,
14807
+ recoverOrder: partial.recoverOrder
14808
+ };
14809
+ }
14810
+ function validateTreeTable(table, config) {
14811
+ const missingColumns = [];
14812
+ const warnings = [];
14813
+ const requiredColumns = [config.parentKey, config.leftKey, config.rightKey];
14814
+ for (const col2 of requiredColumns) {
14815
+ if (!(col2 in table.columns)) {
14816
+ missingColumns.push(col2);
14817
+ }
14818
+ }
14819
+ if (config.depthKey && !(config.depthKey in table.columns)) {
14820
+ warnings.push(`Optional depth column '${config.depthKey}' not found in table '${table.name}'`);
14821
+ }
14822
+ if (config.scope) {
14823
+ for (const scopeCol of config.scope) {
14824
+ if (!(scopeCol in table.columns)) {
14825
+ missingColumns.push(scopeCol);
14826
+ }
14827
+ }
14828
+ }
14829
+ const leftCol = table.columns[config.leftKey];
14830
+ const rightCol = table.columns[config.rightKey];
14831
+ if (leftCol && leftCol.type !== "int" && leftCol.type !== "integer") {
14832
+ warnings.push(`Left column '${config.leftKey}' should be an integer type`);
14833
+ }
14834
+ if (rightCol && rightCol.type !== "int" && rightCol.type !== "integer") {
14835
+ warnings.push(`Right column '${config.rightKey}' should be an integer type`);
14836
+ }
14837
+ return {
14838
+ valid: missingColumns.length === 0,
14839
+ missingColumns,
14840
+ warnings
14841
+ };
14842
+ }
14843
+ function getTreeColumns(table, config) {
14844
+ const parentColumn = table.columns[config.parentKey];
14845
+ const leftColumn = table.columns[config.leftKey];
14846
+ const rightColumn = table.columns[config.rightKey];
14847
+ const depthColumn = config.depthKey ? table.columns[config.depthKey] : void 0;
14848
+ const scopeColumns = (config.scope ?? []).map((s) => table.columns[s]).filter(Boolean);
14849
+ if (!parentColumn || !leftColumn || !rightColumn) {
14850
+ throw new Error(
14851
+ `Table '${table.name}' is missing required tree columns. Required: ${config.parentKey}, ${config.leftKey}, ${config.rightKey}`
14852
+ );
14853
+ }
14854
+ return {
14855
+ parentColumn,
14856
+ leftColumn,
14857
+ rightColumn,
14858
+ depthColumn,
14859
+ scopeColumns
14860
+ };
14861
+ }
14862
+
14863
+ // src/tree/tree-decorator.ts
14864
+ var TREE_METADATA_KEY = /* @__PURE__ */ Symbol("metal-orm:tree");
14865
+ function Tree(options = {}) {
14866
+ return function(target, context) {
14867
+ const config = resolveTreeConfig(options);
14868
+ const metadataBag = readMetadataBag(context) ?? readMetadataBagFromConstructor(target);
14869
+ const metadata = {
14870
+ config,
14871
+ parentProperty: metadataBag?.tree?.parentProperty,
14872
+ childrenProperty: metadataBag?.tree?.childrenProperty
14873
+ };
14874
+ setTreeMetadataOnClass(target, metadata);
14875
+ registerTreeRelations(target, metadata);
14876
+ return target;
14877
+ };
14878
+ }
14879
+ function TreeParent() {
14880
+ return function(_value, context) {
14881
+ if (!context.name) {
14882
+ throw new Error("TreeParent decorator requires a property name");
14883
+ }
14884
+ if (context.private) {
14885
+ throw new Error("TreeParent decorator does not support private fields");
14886
+ }
14887
+ const propertyName = String(context.name);
14888
+ const bag = getOrCreateMetadataBag(context);
14889
+ bag.tree = { ...bag.tree, parentProperty: propertyName };
14890
+ };
14891
+ }
14892
+ function TreeChildren() {
14893
+ return function(_value, context) {
14894
+ if (!context.name) {
14895
+ throw new Error("TreeChildren decorator requires a property name");
14896
+ }
14897
+ if (context.private) {
14898
+ throw new Error("TreeChildren decorator does not support private fields");
14899
+ }
14900
+ const propertyName = String(context.name);
14901
+ const bag = getOrCreateMetadataBag(context);
14902
+ bag.tree = { ...bag.tree, childrenProperty: propertyName };
14903
+ };
14904
+ }
14905
+ function syncTreeEntityMetadata(ctor, treeMetadata) {
14906
+ const current = getTreeMetadata(ctor);
14907
+ if (!current) return;
14908
+ const metadataBag = readMetadataBagFromConstructor(ctor);
14909
+ const nextParentProperty = treeMetadata?.parentProperty ?? metadataBag?.tree?.parentProperty ?? current.parentProperty;
14910
+ const nextChildrenProperty = treeMetadata?.childrenProperty ?? metadataBag?.tree?.childrenProperty ?? current.childrenProperty;
14911
+ if (nextParentProperty !== current.parentProperty || nextChildrenProperty !== current.childrenProperty) {
14912
+ setTreeMetadata(ctor, {
14913
+ ...current,
14914
+ parentProperty: nextParentProperty,
14915
+ childrenProperty: nextChildrenProperty
14916
+ });
14917
+ }
14918
+ registerTreeRelations(ctor, {
14919
+ ...current,
14920
+ parentProperty: nextParentProperty,
14921
+ childrenProperty: nextChildrenProperty
14922
+ });
14923
+ }
14924
+ function getTreeMetadata(target) {
14925
+ return target[TREE_METADATA_KEY];
14926
+ }
14927
+ function setTreeMetadata(target, metadata) {
14928
+ target[TREE_METADATA_KEY] = metadata;
14929
+ }
14930
+ var registerTreeRelations = (target, metadata) => {
14931
+ const entityMeta = getEntityMetadata(target);
14932
+ if (!entityMeta) return;
14933
+ if (metadata.parentProperty && !entityMeta.relations[metadata.parentProperty]) {
14934
+ addRelationMetadata(target, metadata.parentProperty, {
14935
+ kind: RelationKinds.BelongsTo,
14936
+ propertyKey: metadata.parentProperty,
14937
+ target: () => target,
14938
+ foreignKey: metadata.config.parentKey
14939
+ });
14940
+ if (entityMeta.table && !entityMeta.table.relations[metadata.parentProperty]) {
14941
+ entityMeta.table.relations[metadata.parentProperty] = belongsTo(
14942
+ entityMeta.table,
14943
+ metadata.config.parentKey
14944
+ );
14945
+ }
14946
+ }
14947
+ if (metadata.childrenProperty && !entityMeta.relations[metadata.childrenProperty]) {
14948
+ addRelationMetadata(target, metadata.childrenProperty, {
14949
+ kind: RelationKinds.HasMany,
14950
+ propertyKey: metadata.childrenProperty,
14951
+ target: () => target,
14952
+ foreignKey: metadata.config.parentKey
14953
+ });
14954
+ if (entityMeta.table && !entityMeta.table.relations[metadata.childrenProperty]) {
14955
+ entityMeta.table.relations[metadata.childrenProperty] = hasMany(
14956
+ entityMeta.table,
14957
+ metadata.config.parentKey
14958
+ );
14959
+ }
14960
+ }
14961
+ };
14962
+ function setTreeMetadataOnClass(target, metadata) {
14963
+ target[TREE_METADATA_KEY] = metadata;
14964
+ }
14965
+ function hasTreeBehavior(target) {
14966
+ return getTreeMetadata(target) !== void 0;
14967
+ }
14968
+ function getTreeConfig(target) {
14969
+ return getTreeMetadata(target)?.config;
14970
+ }
14971
+ var TreeEntityRegistry = class {
14972
+ entities = /* @__PURE__ */ new Map();
14973
+ tableNames = /* @__PURE__ */ new Map();
14974
+ /**
14975
+ * Registers an entity class with its table name.
14976
+ */
14977
+ register(entityClass, tableName) {
14978
+ this.entities.set(tableName, entityClass);
14979
+ this.tableNames.set(entityClass, tableName);
14980
+ }
14981
+ /**
14982
+ * Gets the entity class for a table name.
14983
+ */
14984
+ getByTableName(tableName) {
14985
+ return this.entities.get(tableName);
14986
+ }
14987
+ /**
14988
+ * Gets the table name for an entity class.
14989
+ */
14990
+ getTableName(entityClass) {
14991
+ return this.tableNames.get(entityClass);
14992
+ }
14993
+ /**
14994
+ * Checks if a table has tree behavior.
14995
+ */
14996
+ isTreeTable(tableName) {
14997
+ const entity = this.entities.get(tableName);
14998
+ return entity ? hasTreeBehavior(entity) : false;
14999
+ }
15000
+ /**
15001
+ * Gets all registered tree entities.
15002
+ */
15003
+ getAll() {
15004
+ const result = [];
15005
+ for (const [tableName, entityClass] of this.entities) {
15006
+ const metadata = getTreeMetadata(entityClass);
15007
+ if (metadata) {
15008
+ result.push({ entityClass, tableName, metadata });
15009
+ }
15010
+ }
15011
+ return result;
15012
+ }
15013
+ /**
15014
+ * Clears the registry.
15015
+ */
15016
+ clear() {
15017
+ this.entities.clear();
15018
+ this.tableNames.clear();
15019
+ }
15020
+ };
15021
+ var treeEntityRegistry = new TreeEntityRegistry();
15022
+ function getTreeBounds(entity, config) {
15023
+ const data = entity;
15024
+ const lft = data[config.leftKey];
15025
+ const rght = data[config.rightKey];
15026
+ if (typeof lft !== "number" || typeof rght !== "number") {
15027
+ return null;
15028
+ }
15029
+ return { lft, rght };
15030
+ }
15031
+ function getTreeParentId(entity, config) {
15032
+ return entity[config.parentKey];
15033
+ }
15034
+ function setTreeBounds(entity, config, lft, rght, depth) {
15035
+ const data = entity;
15036
+ data[config.leftKey] = lft;
15037
+ data[config.rightKey] = rght;
15038
+ if (config.depthKey && depth !== void 0) {
15039
+ data[config.depthKey] = depth;
15040
+ }
15041
+ }
15042
+ function setTreeParentId(entity, config, parentId) {
15043
+ entity[config.parentKey] = parentId;
15044
+ }
15045
+
14569
15046
  // src/decorators/entity.ts
14570
15047
  var toSnakeCase2 = (value) => {
14571
15048
  return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9_]+/gi, "_").replace(/__+/g, "_").replace(/^_|_$/g, "").toLowerCase();
@@ -14609,6 +15086,7 @@ function Entity(options = {}) {
14609
15086
  addRelationMetadata(ctor, entry.propertyName, relationCopy);
14610
15087
  }
14611
15088
  }
15089
+ syncTreeEntityMetadata(ctor, bag?.tree);
14612
15090
  return ctor;
14613
15091
  };
14614
15092
  }
@@ -17535,208 +18013,6 @@ function extractReusableSchemas(schema, existing = {}, prefix = "") {
17535
18013
  return existing;
17536
18014
  }
17537
18015
 
17538
- // src/tree/tree-types.ts
17539
- var DEFAULT_TREE_CONFIG = {
17540
- parentKey: "parentId",
17541
- leftKey: "lft",
17542
- rightKey: "rght",
17543
- cascadeCallbacks: false
17544
- };
17545
- function isTreeConfig(value) {
17546
- if (typeof value !== "object" || value === null) return false;
17547
- const obj = value;
17548
- return typeof obj.parentKey === "string" && typeof obj.leftKey === "string" && typeof obj.rightKey === "string";
17549
- }
17550
- function resolveTreeConfig(partial) {
17551
- return {
17552
- parentKey: partial.parentKey ?? DEFAULT_TREE_CONFIG.parentKey,
17553
- leftKey: partial.leftKey ?? DEFAULT_TREE_CONFIG.leftKey,
17554
- rightKey: partial.rightKey ?? DEFAULT_TREE_CONFIG.rightKey,
17555
- depthKey: partial.depthKey,
17556
- scope: partial.scope,
17557
- cascadeCallbacks: partial.cascadeCallbacks ?? DEFAULT_TREE_CONFIG.cascadeCallbacks,
17558
- recoverOrder: partial.recoverOrder
17559
- };
17560
- }
17561
- function validateTreeTable(table, config) {
17562
- const missingColumns = [];
17563
- const warnings = [];
17564
- const requiredColumns = [config.parentKey, config.leftKey, config.rightKey];
17565
- for (const col2 of requiredColumns) {
17566
- if (!(col2 in table.columns)) {
17567
- missingColumns.push(col2);
17568
- }
17569
- }
17570
- if (config.depthKey && !(config.depthKey in table.columns)) {
17571
- warnings.push(`Optional depth column '${config.depthKey}' not found in table '${table.name}'`);
17572
- }
17573
- if (config.scope) {
17574
- for (const scopeCol of config.scope) {
17575
- if (!(scopeCol in table.columns)) {
17576
- missingColumns.push(scopeCol);
17577
- }
17578
- }
17579
- }
17580
- const leftCol = table.columns[config.leftKey];
17581
- const rightCol = table.columns[config.rightKey];
17582
- if (leftCol && leftCol.type !== "int" && leftCol.type !== "integer") {
17583
- warnings.push(`Left column '${config.leftKey}' should be an integer type`);
17584
- }
17585
- if (rightCol && rightCol.type !== "int" && rightCol.type !== "integer") {
17586
- warnings.push(`Right column '${config.rightKey}' should be an integer type`);
17587
- }
17588
- return {
17589
- valid: missingColumns.length === 0,
17590
- missingColumns,
17591
- warnings
17592
- };
17593
- }
17594
- function getTreeColumns(table, config) {
17595
- const parentColumn = table.columns[config.parentKey];
17596
- const leftColumn = table.columns[config.leftKey];
17597
- const rightColumn = table.columns[config.rightKey];
17598
- const depthColumn = config.depthKey ? table.columns[config.depthKey] : void 0;
17599
- const scopeColumns = (config.scope ?? []).map((s) => table.columns[s]).filter(Boolean);
17600
- if (!parentColumn || !leftColumn || !rightColumn) {
17601
- throw new Error(
17602
- `Table '${table.name}' is missing required tree columns. Required: ${config.parentKey}, ${config.leftKey}, ${config.rightKey}`
17603
- );
17604
- }
17605
- return {
17606
- parentColumn,
17607
- leftColumn,
17608
- rightColumn,
17609
- depthColumn,
17610
- scopeColumns
17611
- };
17612
- }
17613
-
17614
- // src/tree/tree-decorator.ts
17615
- var TREE_METADATA_KEY = /* @__PURE__ */ Symbol("metal-orm:tree");
17616
- function Tree(options = {}) {
17617
- return function(target) {
17618
- const config = resolveTreeConfig(options);
17619
- const metadata = { config };
17620
- setTreeMetadataOnClass(target, metadata);
17621
- return target;
17622
- };
17623
- }
17624
- function TreeParent() {
17625
- return function(_value, context) {
17626
- const propertyName = String(context.name);
17627
- context.addInitializer(function() {
17628
- const constructor = this.constructor;
17629
- const metadata = getTreeMetadata(constructor);
17630
- if (metadata) {
17631
- metadata.parentProperty = propertyName;
17632
- setTreeMetadata(constructor, metadata);
17633
- }
17634
- });
17635
- };
17636
- }
17637
- function TreeChildren() {
17638
- return function(_value, context) {
17639
- const propertyName = String(context.name);
17640
- context.addInitializer(function() {
17641
- const constructor = this.constructor;
17642
- const metadata = getTreeMetadata(constructor);
17643
- if (metadata) {
17644
- metadata.childrenProperty = propertyName;
17645
- setTreeMetadata(constructor, metadata);
17646
- }
17647
- });
17648
- };
17649
- }
17650
- function getTreeMetadata(target) {
17651
- return target[TREE_METADATA_KEY];
17652
- }
17653
- function setTreeMetadata(target, metadata) {
17654
- target[TREE_METADATA_KEY] = metadata;
17655
- }
17656
- function setTreeMetadataOnClass(target, metadata) {
17657
- target[TREE_METADATA_KEY] = metadata;
17658
- }
17659
- function hasTreeBehavior(target) {
17660
- return getTreeMetadata(target) !== void 0;
17661
- }
17662
- function getTreeConfig(target) {
17663
- return getTreeMetadata(target)?.config;
17664
- }
17665
- var TreeEntityRegistry = class {
17666
- entities = /* @__PURE__ */ new Map();
17667
- tableNames = /* @__PURE__ */ new Map();
17668
- /**
17669
- * Registers an entity class with its table name.
17670
- */
17671
- register(entityClass, tableName) {
17672
- this.entities.set(tableName, entityClass);
17673
- this.tableNames.set(entityClass, tableName);
17674
- }
17675
- /**
17676
- * Gets the entity class for a table name.
17677
- */
17678
- getByTableName(tableName) {
17679
- return this.entities.get(tableName);
17680
- }
17681
- /**
17682
- * Gets the table name for an entity class.
17683
- */
17684
- getTableName(entityClass) {
17685
- return this.tableNames.get(entityClass);
17686
- }
17687
- /**
17688
- * Checks if a table has tree behavior.
17689
- */
17690
- isTreeTable(tableName) {
17691
- const entity = this.entities.get(tableName);
17692
- return entity ? hasTreeBehavior(entity) : false;
17693
- }
17694
- /**
17695
- * Gets all registered tree entities.
17696
- */
17697
- getAll() {
17698
- const result = [];
17699
- for (const [tableName, entityClass] of this.entities) {
17700
- const metadata = getTreeMetadata(entityClass);
17701
- if (metadata) {
17702
- result.push({ entityClass, tableName, metadata });
17703
- }
17704
- }
17705
- return result;
17706
- }
17707
- /**
17708
- * Clears the registry.
17709
- */
17710
- clear() {
17711
- this.entities.clear();
17712
- this.tableNames.clear();
17713
- }
17714
- };
17715
- var treeEntityRegistry = new TreeEntityRegistry();
17716
- function getTreeBounds(entity, config) {
17717
- const data = entity;
17718
- const lft = data[config.leftKey];
17719
- const rght = data[config.rightKey];
17720
- if (typeof lft !== "number" || typeof rght !== "number") {
17721
- return null;
17722
- }
17723
- return { lft, rght };
17724
- }
17725
- function getTreeParentId(entity, config) {
17726
- return entity[config.parentKey];
17727
- }
17728
- function setTreeBounds(entity, config, lft, rght, depth) {
17729
- const data = entity;
17730
- data[config.leftKey] = lft;
17731
- data[config.rightKey] = rght;
17732
- if (config.depthKey && depth !== void 0) {
17733
- data[config.depthKey] = depth;
17734
- }
17735
- }
17736
- function setTreeParentId(entity, config, parentId) {
17737
- entity[config.parentKey] = parentId;
17738
- }
17739
-
17740
18016
  // src/dto/openapi/generators/tree.ts
17741
18017
  function threadedNodeToOpenApiSchema(target, options) {
17742
18018
  const componentName = options?.componentName;
@@ -19760,6 +20036,7 @@ export {
19760
20036
  sub,
19761
20037
  substr,
19762
20038
  sum,
20039
+ syncTreeEntityMetadata,
19763
20040
  synchronizeSchema,
19764
20041
  tableRef,
19765
20042
  tan,