metal-orm 1.0.114 → 1.0.115

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 CHANGED
@@ -79,10 +79,12 @@ Full docs live in the `docs/` folder:
79
79
  - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
80
80
  - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
81
81
  - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
82
- - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
83
- - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
84
- - [Tree Behavior (Nested Set/MPTT)](https://github.com/celsowm/metal-orm/blob/main/docs/tree.md)
85
- - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
82
+ - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
83
+ - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
84
+ - [Tree Behavior (Nested Set/MPTT)](https://github.com/celsowm/metal-orm/blob/main/docs/tree.md)
85
+ - [DTO (Data Transfer Objects)](https://github.com/celsowm/metal-orm/blob/main/docs/dto.md)
86
+ - [OpenAPI Schema Generation](https://github.com/celsowm/metal-orm/blob/main/docs/openapi.md)
87
+ - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
86
88
  - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
87
89
  - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
88
90
  - [Save Graph](https://github.com/celsowm/metal-orm/blob/main/docs/save-graph.md)
@@ -124,10 +126,11 @@ On top of the query builder, MetalORM ships a focused runtime managed by `Orm` a
124
126
 
125
127
  - **Entities inferred from your `TableDef`s** (no separate mapping file).
126
128
  - **Lazy, batched relations**: `user.posts.load()`, `user.roles.syncByIds([...])`, etc.
127
- - **Scoped transactions**: `session.transaction(async s => { ... })` wraps `begin/commit/rollback` on the existing executor; `Orm.transaction` remains available when you want a fresh transactional executor per call.
128
- - **Identity map**: the same row becomes the same entity instance within a session (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
129
- - **Tree Behavior (Nested Set/MPTT)**: hierarchical data with `TreeManager`, `treeQuery()`, and `@Tree` decorators. Efficient O(log n) operations for moves, inserts, and deletes. Supports multi-tree scoping, recovery, and validation.
130
- - **Unit of Work (`OrmSession`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/Unit_of_work).
129
+ - **Scoped transactions**: `session.transaction(async s => { ... })` wraps `begin/commit/rollback` on the existing executor; `Orm.transaction` remains available when you want a fresh transactional executor per call.
130
+ - **Identity map**: the same row becomes the same entity instance within a session (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
131
+ - **Tree Behavior (Nested Set/MPTT)**: hierarchical data with `TreeManager`, `treeQuery()`, and `@Tree` decorators. Efficient O(log n) operations for moves, inserts, and deletes. Supports multi-tree scoping, recovery, and validation.
132
+ - **DTO/OpenAPI helpers**: the `metal-orm/dto` module generates DTOs and OpenAPI schemas, including tree schemas (`TreeNode`, `TreeNodeResult`, threaded trees).
133
+ - **Unit of Work (`OrmSession`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/Unit_of_work).
131
134
  - **Graph persistence**: mutate a whole object graph and flush once with `session.commit()`.
132
135
  - **Partial updates**: use `session.patchGraph()` to update only specific fields of an entity and its relations (returns `null` if entity doesn't exist).
133
136
  - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
package/dist/index.cjs CHANGED
@@ -218,6 +218,7 @@ __export(index_exports, {
218
218
  generateRelationComponents: () => generateRelationComponents,
219
219
  generateSchemaSql: () => generateSchemaSql,
220
220
  generateSchemaSqlFor: () => generateSchemaSqlFor,
221
+ generateTreeComponents: () => generateTreeComponents,
221
222
  getColumn: () => getColumn,
222
223
  getColumnMap: () => getColumnMap,
223
224
  getColumnType: () => getColumnType,
@@ -381,6 +382,7 @@ __export(index_exports, {
381
382
  tableRef: () => tableRef,
382
383
  tan: () => tan,
383
384
  threadResults: () => threadResults,
385
+ threadedNodeToOpenApiSchema: () => threadedNodeToOpenApiSchema,
384
386
  toColumnRef: () => toColumnRef,
385
387
  toPagedResponse: () => toPagedResponse,
386
388
  toPagedResponseBuilder: () => toPagedResponseBuilder,
@@ -389,6 +391,9 @@ __export(index_exports, {
389
391
  toResponseBuilder: () => toResponseBuilder,
390
392
  toTableRef: () => toTableRef,
391
393
  treeEntityRegistry: () => treeEntityRegistry,
394
+ treeListEntryToOpenApiSchema: () => treeListEntryToOpenApiSchema,
395
+ treeNodeResultToOpenApiSchema: () => treeNodeResultToOpenApiSchema,
396
+ treeNodeToOpenApiSchema: () => treeNodeToOpenApiSchema,
392
397
  treeQuery: () => treeQuery,
393
398
  trim: () => trim,
394
399
  trunc: () => trunc,
@@ -15966,6 +15971,406 @@ function extractReusableSchemas(schema, existing = {}, prefix = "") {
15966
15971
  return existing;
15967
15972
  }
15968
15973
 
15974
+ // src/tree/tree-types.ts
15975
+ var DEFAULT_TREE_CONFIG = {
15976
+ parentKey: "parentId",
15977
+ leftKey: "lft",
15978
+ rightKey: "rght",
15979
+ cascadeCallbacks: false
15980
+ };
15981
+ function isTreeConfig(value) {
15982
+ if (typeof value !== "object" || value === null) return false;
15983
+ const obj = value;
15984
+ return typeof obj.parentKey === "string" && typeof obj.leftKey === "string" && typeof obj.rightKey === "string";
15985
+ }
15986
+ function resolveTreeConfig(partial) {
15987
+ return {
15988
+ parentKey: partial.parentKey ?? DEFAULT_TREE_CONFIG.parentKey,
15989
+ leftKey: partial.leftKey ?? DEFAULT_TREE_CONFIG.leftKey,
15990
+ rightKey: partial.rightKey ?? DEFAULT_TREE_CONFIG.rightKey,
15991
+ depthKey: partial.depthKey,
15992
+ scope: partial.scope,
15993
+ cascadeCallbacks: partial.cascadeCallbacks ?? DEFAULT_TREE_CONFIG.cascadeCallbacks,
15994
+ recoverOrder: partial.recoverOrder
15995
+ };
15996
+ }
15997
+ function validateTreeTable(table, config) {
15998
+ const missingColumns = [];
15999
+ const warnings = [];
16000
+ const requiredColumns = [config.parentKey, config.leftKey, config.rightKey];
16001
+ for (const col2 of requiredColumns) {
16002
+ if (!(col2 in table.columns)) {
16003
+ missingColumns.push(col2);
16004
+ }
16005
+ }
16006
+ if (config.depthKey && !(config.depthKey in table.columns)) {
16007
+ warnings.push(`Optional depth column '${config.depthKey}' not found in table '${table.name}'`);
16008
+ }
16009
+ if (config.scope) {
16010
+ for (const scopeCol of config.scope) {
16011
+ if (!(scopeCol in table.columns)) {
16012
+ missingColumns.push(scopeCol);
16013
+ }
16014
+ }
16015
+ }
16016
+ const leftCol = table.columns[config.leftKey];
16017
+ const rightCol = table.columns[config.rightKey];
16018
+ if (leftCol && leftCol.type !== "int" && leftCol.type !== "integer") {
16019
+ warnings.push(`Left column '${config.leftKey}' should be an integer type`);
16020
+ }
16021
+ if (rightCol && rightCol.type !== "int" && rightCol.type !== "integer") {
16022
+ warnings.push(`Right column '${config.rightKey}' should be an integer type`);
16023
+ }
16024
+ return {
16025
+ valid: missingColumns.length === 0,
16026
+ missingColumns,
16027
+ warnings
16028
+ };
16029
+ }
16030
+ function getTreeColumns(table, config) {
16031
+ const parentColumn = table.columns[config.parentKey];
16032
+ const leftColumn = table.columns[config.leftKey];
16033
+ const rightColumn = table.columns[config.rightKey];
16034
+ const depthColumn = config.depthKey ? table.columns[config.depthKey] : void 0;
16035
+ const scopeColumns = (config.scope ?? []).map((s) => table.columns[s]).filter(Boolean);
16036
+ if (!parentColumn || !leftColumn || !rightColumn) {
16037
+ throw new Error(
16038
+ `Table '${table.name}' is missing required tree columns. Required: ${config.parentKey}, ${config.leftKey}, ${config.rightKey}`
16039
+ );
16040
+ }
16041
+ return {
16042
+ parentColumn,
16043
+ leftColumn,
16044
+ rightColumn,
16045
+ depthColumn,
16046
+ scopeColumns
16047
+ };
16048
+ }
16049
+
16050
+ // src/tree/tree-decorator.ts
16051
+ var TREE_METADATA_KEY = /* @__PURE__ */ Symbol("metal-orm:tree");
16052
+ function Tree(options = {}) {
16053
+ return function(target) {
16054
+ const config = resolveTreeConfig(options);
16055
+ const metadata = { config };
16056
+ setTreeMetadataOnClass(target, metadata);
16057
+ return target;
16058
+ };
16059
+ }
16060
+ function TreeParent() {
16061
+ return function(_value, context) {
16062
+ const propertyName = String(context.name);
16063
+ context.addInitializer(function() {
16064
+ const constructor = this.constructor;
16065
+ const metadata = getTreeMetadata(constructor);
16066
+ if (metadata) {
16067
+ metadata.parentProperty = propertyName;
16068
+ setTreeMetadata(constructor, metadata);
16069
+ }
16070
+ });
16071
+ };
16072
+ }
16073
+ function TreeChildren() {
16074
+ return function(_value, context) {
16075
+ const propertyName = String(context.name);
16076
+ context.addInitializer(function() {
16077
+ const constructor = this.constructor;
16078
+ const metadata = getTreeMetadata(constructor);
16079
+ if (metadata) {
16080
+ metadata.childrenProperty = propertyName;
16081
+ setTreeMetadata(constructor, metadata);
16082
+ }
16083
+ });
16084
+ };
16085
+ }
16086
+ function getTreeMetadata(target) {
16087
+ return target[TREE_METADATA_KEY];
16088
+ }
16089
+ function setTreeMetadata(target, metadata) {
16090
+ target[TREE_METADATA_KEY] = metadata;
16091
+ }
16092
+ function setTreeMetadataOnClass(target, metadata) {
16093
+ target[TREE_METADATA_KEY] = metadata;
16094
+ }
16095
+ function hasTreeBehavior(target) {
16096
+ return getTreeMetadata(target) !== void 0;
16097
+ }
16098
+ function getTreeConfig(target) {
16099
+ return getTreeMetadata(target)?.config;
16100
+ }
16101
+ var TreeEntityRegistry = class {
16102
+ entities = /* @__PURE__ */ new Map();
16103
+ tableNames = /* @__PURE__ */ new Map();
16104
+ /**
16105
+ * Registers an entity class with its table name.
16106
+ */
16107
+ register(entityClass, tableName) {
16108
+ this.entities.set(tableName, entityClass);
16109
+ this.tableNames.set(entityClass, tableName);
16110
+ }
16111
+ /**
16112
+ * Gets the entity class for a table name.
16113
+ */
16114
+ getByTableName(tableName) {
16115
+ return this.entities.get(tableName);
16116
+ }
16117
+ /**
16118
+ * Gets the table name for an entity class.
16119
+ */
16120
+ getTableName(entityClass) {
16121
+ return this.tableNames.get(entityClass);
16122
+ }
16123
+ /**
16124
+ * Checks if a table has tree behavior.
16125
+ */
16126
+ isTreeTable(tableName) {
16127
+ const entity = this.entities.get(tableName);
16128
+ return entity ? hasTreeBehavior(entity) : false;
16129
+ }
16130
+ /**
16131
+ * Gets all registered tree entities.
16132
+ */
16133
+ getAll() {
16134
+ const result = [];
16135
+ for (const [tableName, entityClass] of this.entities) {
16136
+ const metadata = getTreeMetadata(entityClass);
16137
+ if (metadata) {
16138
+ result.push({ entityClass, tableName, metadata });
16139
+ }
16140
+ }
16141
+ return result;
16142
+ }
16143
+ /**
16144
+ * Clears the registry.
16145
+ */
16146
+ clear() {
16147
+ this.entities.clear();
16148
+ this.tableNames.clear();
16149
+ }
16150
+ };
16151
+ var treeEntityRegistry = new TreeEntityRegistry();
16152
+ function getTreeBounds(entity, config) {
16153
+ const data = entity;
16154
+ const lft = data[config.leftKey];
16155
+ const rght = data[config.rightKey];
16156
+ if (typeof lft !== "number" || typeof rght !== "number") {
16157
+ return null;
16158
+ }
16159
+ return { lft, rght };
16160
+ }
16161
+ function getTreeParentId(entity, config) {
16162
+ return entity[config.parentKey];
16163
+ }
16164
+ function setTreeBounds(entity, config, lft, rght, depth) {
16165
+ const data = entity;
16166
+ data[config.leftKey] = lft;
16167
+ data[config.rightKey] = rght;
16168
+ if (config.depthKey && depth !== void 0) {
16169
+ data[config.depthKey] = depth;
16170
+ }
16171
+ }
16172
+ function setTreeParentId(entity, config, parentId) {
16173
+ entity[config.parentKey] = parentId;
16174
+ }
16175
+
16176
+ // src/dto/openapi/generators/tree.ts
16177
+ function threadedNodeToOpenApiSchema(target, options) {
16178
+ const componentName = options?.componentName;
16179
+ const nodeSchema = entityToOpenApiSchema(target, options);
16180
+ const treeNodeSchema = {
16181
+ type: "object",
16182
+ properties: {
16183
+ node: nodeSchema,
16184
+ children: {
16185
+ type: "array",
16186
+ items: componentName ? { $ref: `#/components/schemas/${componentName}` } : {},
16187
+ description: "Child nodes in the tree hierarchy"
16188
+ }
16189
+ },
16190
+ required: ["node", "children"],
16191
+ description: "A node in a threaded tree structure with nested children"
16192
+ };
16193
+ return treeNodeSchema;
16194
+ }
16195
+ function treeNodeToOpenApiSchema(target, options) {
16196
+ const includeMetadata = options?.includeTreeMetadata ?? true;
16197
+ const entitySchema = entityToOpenApiSchema(target, options);
16198
+ const properties = {
16199
+ entity: entitySchema,
16200
+ lft: {
16201
+ type: "integer",
16202
+ description: "Left boundary value (nested set)"
16203
+ },
16204
+ rght: {
16205
+ type: "integer",
16206
+ description: "Right boundary value (nested set)"
16207
+ },
16208
+ isLeaf: {
16209
+ type: "boolean",
16210
+ description: "Whether this node has no children"
16211
+ },
16212
+ isRoot: {
16213
+ type: "boolean",
16214
+ description: "Whether this node has no parent"
16215
+ },
16216
+ childCount: {
16217
+ type: "integer",
16218
+ minimum: 0,
16219
+ description: "Number of descendants"
16220
+ }
16221
+ };
16222
+ if (includeMetadata) {
16223
+ properties.depth = {
16224
+ type: "integer",
16225
+ minimum: 0,
16226
+ description: "Depth level (0 = root)"
16227
+ };
16228
+ }
16229
+ return {
16230
+ type: "object",
16231
+ properties,
16232
+ required: ["entity", "lft", "rght", "isLeaf", "isRoot", "childCount"],
16233
+ description: "A tree node with nested set boundaries and metadata"
16234
+ };
16235
+ }
16236
+ function treeNodeResultToOpenApiSchema(target, options) {
16237
+ const includeMetadata = options?.includeTreeMetadata ?? true;
16238
+ const entitySchema = entityToOpenApiSchema(target, options);
16239
+ const parentKey = resolveParentKey(target, options);
16240
+ const parentSchema = resolveParentSchema(target, parentKey, options?.dialect);
16241
+ const parentIdSchema = {
16242
+ ...parentSchema,
16243
+ description: parentSchema.description ?? "Parent identifier (null for roots)"
16244
+ };
16245
+ const properties = {
16246
+ data: entitySchema,
16247
+ lft: {
16248
+ type: "integer",
16249
+ description: "Left boundary value (nested set)"
16250
+ },
16251
+ rght: {
16252
+ type: "integer",
16253
+ description: "Right boundary value (nested set)"
16254
+ },
16255
+ parentId: parentIdSchema,
16256
+ isLeaf: {
16257
+ type: "boolean",
16258
+ description: "Whether this node has no children"
16259
+ },
16260
+ isRoot: {
16261
+ type: "boolean",
16262
+ description: "Whether this node has no parent"
16263
+ }
16264
+ };
16265
+ if (includeMetadata) {
16266
+ properties.depth = {
16267
+ type: "integer",
16268
+ minimum: 0,
16269
+ description: "Depth level (0 = root)"
16270
+ };
16271
+ }
16272
+ return {
16273
+ type: "object",
16274
+ properties,
16275
+ required: ["data", "lft", "rght", "parentId", "isLeaf", "isRoot"],
16276
+ description: "A tree node result with nested set boundaries and metadata"
16277
+ };
16278
+ }
16279
+ function treeListEntryToOpenApiSchema(options) {
16280
+ const keyType = options?.keyType ?? "integer";
16281
+ const valueType = options?.valueType ?? "string";
16282
+ return {
16283
+ type: "object",
16284
+ properties: {
16285
+ key: {
16286
+ type: keyType,
16287
+ description: "The key (usually primary key)"
16288
+ },
16289
+ value: {
16290
+ type: valueType,
16291
+ description: "The display value with depth prefix"
16292
+ },
16293
+ depth: {
16294
+ type: "integer",
16295
+ minimum: 0,
16296
+ description: "The depth level"
16297
+ }
16298
+ },
16299
+ required: ["key", "value", "depth"],
16300
+ description: "A tree list entry for dropdown/select rendering"
16301
+ };
16302
+ }
16303
+ function generateTreeComponents(target, baseName, options) {
16304
+ const threadedNodeName = `${baseName}TreeNode`;
16305
+ return {
16306
+ [baseName]: entityToOpenApiSchema(target, options),
16307
+ [`${baseName}Node`]: treeNodeToOpenApiSchema(target, options),
16308
+ [`${baseName}NodeResult`]: treeNodeResultToOpenApiSchema(target, options),
16309
+ [threadedNodeName]: threadedNodeToOpenApiSchema(target, {
16310
+ ...options,
16311
+ componentName: threadedNodeName
16312
+ }),
16313
+ [`${baseName}TreeList`]: {
16314
+ type: "array",
16315
+ items: treeListEntryToOpenApiSchema(),
16316
+ description: `Flat list of ${baseName} tree entries for dropdown/select`
16317
+ },
16318
+ [`${baseName}ThreadedTree`]: {
16319
+ type: "array",
16320
+ items: { $ref: `#/components/schemas/${threadedNodeName}` },
16321
+ description: `Threaded tree structure of ${baseName} nodes`
16322
+ }
16323
+ };
16324
+ }
16325
+ function entityToOpenApiSchema(target, options) {
16326
+ const columns = getColumnMap(target);
16327
+ const properties = {};
16328
+ const required = [];
16329
+ const dialect = options?.dialect ?? "openapi-3.1";
16330
+ for (const [key, col2] of Object.entries(columns)) {
16331
+ if (options?.exclude?.includes(key)) {
16332
+ continue;
16333
+ }
16334
+ if (options?.include && !options.include.includes(key)) {
16335
+ continue;
16336
+ }
16337
+ properties[key] = columnToOpenApiSchema(col2, dialect);
16338
+ if (col2.notNull || col2.primary) {
16339
+ required.push(key);
16340
+ }
16341
+ }
16342
+ return {
16343
+ type: "object",
16344
+ properties,
16345
+ ...required.length > 0 && { required }
16346
+ };
16347
+ }
16348
+ function resolveParentSchema(target, parentKey, dialect = "openapi-3.1") {
16349
+ const columns = getColumnMap(target);
16350
+ const parentColumn = columns[parentKey] ?? findColumnByName(columns, parentKey);
16351
+ if (parentColumn) {
16352
+ return columnToOpenApiSchema(parentColumn, dialect);
16353
+ }
16354
+ return {
16355
+ type: ["string", "null"]
16356
+ };
16357
+ }
16358
+ function resolveParentKey(target, options) {
16359
+ if (options?.parentKey) {
16360
+ return options.parentKey;
16361
+ }
16362
+ if (!isTableDef2(target)) {
16363
+ const config = getTreeConfig(target);
16364
+ if (config?.parentKey) {
16365
+ return config.parentKey;
16366
+ }
16367
+ }
16368
+ return "parentId";
16369
+ }
16370
+ function findColumnByName(columns, columnName) {
16371
+ return Object.values(columns).find((col2) => col2.name === columnName);
16372
+ }
16373
+
15969
16374
  // src/dto/openapi/generators/pagination.ts
15970
16375
  var paginationParamsSchema = {
15971
16376
  type: "object",
@@ -16066,82 +16471,6 @@ function pagedResponseToOpenApiSchema(itemSchema) {
16066
16471
  };
16067
16472
  }
16068
16473
 
16069
- // src/tree/tree-types.ts
16070
- var DEFAULT_TREE_CONFIG = {
16071
- parentKey: "parentId",
16072
- leftKey: "lft",
16073
- rightKey: "rght",
16074
- cascadeCallbacks: false
16075
- };
16076
- function isTreeConfig(value) {
16077
- if (typeof value !== "object" || value === null) return false;
16078
- const obj = value;
16079
- return typeof obj.parentKey === "string" && typeof obj.leftKey === "string" && typeof obj.rightKey === "string";
16080
- }
16081
- function resolveTreeConfig(partial) {
16082
- return {
16083
- parentKey: partial.parentKey ?? DEFAULT_TREE_CONFIG.parentKey,
16084
- leftKey: partial.leftKey ?? DEFAULT_TREE_CONFIG.leftKey,
16085
- rightKey: partial.rightKey ?? DEFAULT_TREE_CONFIG.rightKey,
16086
- depthKey: partial.depthKey,
16087
- scope: partial.scope,
16088
- cascadeCallbacks: partial.cascadeCallbacks ?? DEFAULT_TREE_CONFIG.cascadeCallbacks,
16089
- recoverOrder: partial.recoverOrder
16090
- };
16091
- }
16092
- function validateTreeTable(table, config) {
16093
- const missingColumns = [];
16094
- const warnings = [];
16095
- const requiredColumns = [config.parentKey, config.leftKey, config.rightKey];
16096
- for (const col2 of requiredColumns) {
16097
- if (!(col2 in table.columns)) {
16098
- missingColumns.push(col2);
16099
- }
16100
- }
16101
- if (config.depthKey && !(config.depthKey in table.columns)) {
16102
- warnings.push(`Optional depth column '${config.depthKey}' not found in table '${table.name}'`);
16103
- }
16104
- if (config.scope) {
16105
- for (const scopeCol of config.scope) {
16106
- if (!(scopeCol in table.columns)) {
16107
- missingColumns.push(scopeCol);
16108
- }
16109
- }
16110
- }
16111
- const leftCol = table.columns[config.leftKey];
16112
- const rightCol = table.columns[config.rightKey];
16113
- if (leftCol && leftCol.type !== "int" && leftCol.type !== "integer") {
16114
- warnings.push(`Left column '${config.leftKey}' should be an integer type`);
16115
- }
16116
- if (rightCol && rightCol.type !== "int" && rightCol.type !== "integer") {
16117
- warnings.push(`Right column '${config.rightKey}' should be an integer type`);
16118
- }
16119
- return {
16120
- valid: missingColumns.length === 0,
16121
- missingColumns,
16122
- warnings
16123
- };
16124
- }
16125
- function getTreeColumns(table, config) {
16126
- const parentColumn = table.columns[config.parentKey];
16127
- const leftColumn = table.columns[config.leftKey];
16128
- const rightColumn = table.columns[config.rightKey];
16129
- const depthColumn = config.depthKey ? table.columns[config.depthKey] : void 0;
16130
- const scopeColumns = (config.scope ?? []).map((s) => table.columns[s]).filter(Boolean);
16131
- if (!parentColumn || !leftColumn || !rightColumn) {
16132
- throw new Error(
16133
- `Table '${table.name}' is missing required tree columns. Required: ${config.parentKey}, ${config.leftKey}, ${config.rightKey}`
16134
- );
16135
- }
16136
- return {
16137
- parentColumn,
16138
- leftColumn,
16139
- rightColumn,
16140
- depthColumn,
16141
- scopeColumns
16142
- };
16143
- }
16144
-
16145
16474
  // src/tree/nested-set-strategy.ts
16146
16475
  var NestedSetStrategy = class {
16147
16476
  /**
@@ -17207,132 +17536,6 @@ function queryResultsToRows(results) {
17207
17536
  }
17208
17537
  return rows;
17209
17538
  }
17210
-
17211
- // src/tree/tree-decorator.ts
17212
- var TREE_METADATA_KEY = /* @__PURE__ */ Symbol("metal-orm:tree");
17213
- function Tree(options = {}) {
17214
- return function(target) {
17215
- const config = resolveTreeConfig(options);
17216
- const metadata = { config };
17217
- setTreeMetadataOnClass(target, metadata);
17218
- return target;
17219
- };
17220
- }
17221
- function TreeParent() {
17222
- return function(_value, context) {
17223
- const propertyName = String(context.name);
17224
- context.addInitializer(function() {
17225
- const constructor = this.constructor;
17226
- const metadata = getTreeMetadata(constructor);
17227
- if (metadata) {
17228
- metadata.parentProperty = propertyName;
17229
- setTreeMetadata(constructor, metadata);
17230
- }
17231
- });
17232
- };
17233
- }
17234
- function TreeChildren() {
17235
- return function(_value, context) {
17236
- const propertyName = String(context.name);
17237
- context.addInitializer(function() {
17238
- const constructor = this.constructor;
17239
- const metadata = getTreeMetadata(constructor);
17240
- if (metadata) {
17241
- metadata.childrenProperty = propertyName;
17242
- setTreeMetadata(constructor, metadata);
17243
- }
17244
- });
17245
- };
17246
- }
17247
- function getTreeMetadata(target) {
17248
- return target[TREE_METADATA_KEY];
17249
- }
17250
- function setTreeMetadata(target, metadata) {
17251
- target[TREE_METADATA_KEY] = metadata;
17252
- }
17253
- function setTreeMetadataOnClass(target, metadata) {
17254
- target[TREE_METADATA_KEY] = metadata;
17255
- }
17256
- function hasTreeBehavior(target) {
17257
- return getTreeMetadata(target) !== void 0;
17258
- }
17259
- function getTreeConfig(target) {
17260
- return getTreeMetadata(target)?.config;
17261
- }
17262
- var TreeEntityRegistry = class {
17263
- entities = /* @__PURE__ */ new Map();
17264
- tableNames = /* @__PURE__ */ new Map();
17265
- /**
17266
- * Registers an entity class with its table name.
17267
- */
17268
- register(entityClass, tableName) {
17269
- this.entities.set(tableName, entityClass);
17270
- this.tableNames.set(entityClass, tableName);
17271
- }
17272
- /**
17273
- * Gets the entity class for a table name.
17274
- */
17275
- getByTableName(tableName) {
17276
- return this.entities.get(tableName);
17277
- }
17278
- /**
17279
- * Gets the table name for an entity class.
17280
- */
17281
- getTableName(entityClass) {
17282
- return this.tableNames.get(entityClass);
17283
- }
17284
- /**
17285
- * Checks if a table has tree behavior.
17286
- */
17287
- isTreeTable(tableName) {
17288
- const entity = this.entities.get(tableName);
17289
- return entity ? hasTreeBehavior(entity) : false;
17290
- }
17291
- /**
17292
- * Gets all registered tree entities.
17293
- */
17294
- getAll() {
17295
- const result = [];
17296
- for (const [tableName, entityClass] of this.entities) {
17297
- const metadata = getTreeMetadata(entityClass);
17298
- if (metadata) {
17299
- result.push({ entityClass, tableName, metadata });
17300
- }
17301
- }
17302
- return result;
17303
- }
17304
- /**
17305
- * Clears the registry.
17306
- */
17307
- clear() {
17308
- this.entities.clear();
17309
- this.tableNames.clear();
17310
- }
17311
- };
17312
- var treeEntityRegistry = new TreeEntityRegistry();
17313
- function getTreeBounds(entity, config) {
17314
- const data = entity;
17315
- const lft = data[config.leftKey];
17316
- const rght = data[config.rightKey];
17317
- if (typeof lft !== "number" || typeof rght !== "number") {
17318
- return null;
17319
- }
17320
- return { lft, rght };
17321
- }
17322
- function getTreeParentId(entity, config) {
17323
- return entity[config.parentKey];
17324
- }
17325
- function setTreeBounds(entity, config, lft, rght, depth) {
17326
- const data = entity;
17327
- data[config.leftKey] = lft;
17328
- data[config.rightKey] = rght;
17329
- if (config.depthKey && depth !== void 0) {
17330
- data[config.depthKey] = depth;
17331
- }
17332
- }
17333
- function setTreeParentId(entity, config, parentId) {
17334
- entity[config.parentKey] = parentId;
17335
- }
17336
17539
  // Annotate the CommonJS export names for ESM import in node:
17337
17540
  0 && (module.exports = {
17338
17541
  Alphanumeric,
@@ -17510,6 +17713,7 @@ function setTreeParentId(entity, config, parentId) {
17510
17713
  generateRelationComponents,
17511
17714
  generateSchemaSql,
17512
17715
  generateSchemaSqlFor,
17716
+ generateTreeComponents,
17513
17717
  getColumn,
17514
17718
  getColumnMap,
17515
17719
  getColumnType,
@@ -17673,6 +17877,7 @@ function setTreeParentId(entity, config, parentId) {
17673
17877
  tableRef,
17674
17878
  tan,
17675
17879
  threadResults,
17880
+ threadedNodeToOpenApiSchema,
17676
17881
  toColumnRef,
17677
17882
  toPagedResponse,
17678
17883
  toPagedResponseBuilder,
@@ -17681,6 +17886,9 @@ function setTreeParentId(entity, config, parentId) {
17681
17886
  toResponseBuilder,
17682
17887
  toTableRef,
17683
17888
  treeEntityRegistry,
17889
+ treeListEntryToOpenApiSchema,
17890
+ treeNodeResultToOpenApiSchema,
17891
+ treeNodeToOpenApiSchema,
17684
17892
  treeQuery,
17685
17893
  trim,
17686
17894
  trunc,