metal-orm 1.1.21 → 1.1.23
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 +379 -474
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -173
- package/dist/index.d.ts +145 -173
- package/dist/index.js +374 -474
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ddl/introspect/catalogs/mssql.ts +0 -8
- package/src/core/ddl/introspect/catalogs/mysql.ts +0 -5
- package/src/core/ddl/introspect/catalogs/postgres.ts +0 -9
- package/src/core/ddl/introspect/catalogs/sqlite.ts +0 -3
- package/src/core/dialect/abstract.ts +94 -417
- package/src/core/dialect/base/expression-compiler-registry.ts +254 -0
- package/src/core/dialect/base/function-table-formatter.ts +40 -78
- package/src/core/dialect/base/select-ast-normalizer.ts +61 -0
- package/src/core/dialect/base/sql-dialect.ts +36 -59
- package/src/core/dialect/capabilities/procedure-compiler.ts +30 -0
- package/src/core/dialect/dialect-factory.ts +3 -4
- package/src/core/dialect/mssql/index.ts +3 -2
- package/src/core/dialect/mysql/index.ts +3 -2
- package/src/core/dialect/postgres/index.ts +3 -2
- package/src/core/dialect/sqlite/index.ts +1 -7
- package/src/decorators/entity.ts +2 -3
- package/src/index.ts +4 -0
- package/src/orm/entity-metadata.ts +6 -11
- package/src/orm/execute-procedure.ts +3 -2
- package/src/orm/lifecycle.ts +20 -0
- package/src/orm/orm-session.ts +37 -3
- package/src/orm/unit-of-work.ts +15 -9
- package/src/query-builder/procedure-call.ts +4 -18
- package/src/schema/table.ts +10 -25
- package/src/tree/tree-manager.ts +111 -43
package/src/tree/tree-manager.ts
CHANGED
|
@@ -279,7 +279,7 @@ export class TreeManager<T extends TableDef> {
|
|
|
279
279
|
* Moves a node to be the last child of a new parent.
|
|
280
280
|
*/
|
|
281
281
|
async moveTo(node: TreeNodeResult, newParentId: unknown | null): Promise<void> {
|
|
282
|
-
NestedSetStrategy.subtreeWidth(node.lft, node.rght);
|
|
282
|
+
const width = NestedSetStrategy.subtreeWidth(node.lft, node.rght);
|
|
283
283
|
|
|
284
284
|
let newPos: { lft: number; rght: number; depth: number };
|
|
285
285
|
|
|
@@ -297,7 +297,13 @@ export class TreeManager<T extends TableDef> {
|
|
|
297
297
|
);
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
|
|
300
|
+
// If the destination was to the right of the subtree, closing the old gap
|
|
301
|
+
// shifts that destination left by the subtree width before we reopen it.
|
|
302
|
+
const targetLft = newPos.lft > node.rght
|
|
303
|
+
? newPos.lft - width
|
|
304
|
+
: newPos.lft;
|
|
305
|
+
|
|
306
|
+
await this.moveSubtree(node, targetLft, newParentId, newPos.depth);
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
/**
|
|
@@ -350,9 +356,14 @@ export class TreeManager<T extends TableDef> {
|
|
|
350
356
|
return insertData[this.pkName];
|
|
351
357
|
}
|
|
352
358
|
|
|
353
|
-
// For auto-increment, query for the inserted node by its
|
|
354
|
-
|
|
355
|
-
|
|
359
|
+
// For auto-increment, query for the inserted node by its lft value inside
|
|
360
|
+
// the same tree scope. Different scoped trees legitimately reuse lft values.
|
|
361
|
+
const scopeExpressions = this.getScopeExpressions();
|
|
362
|
+
const lftCondition = eq(this.table.columns[this.config.leftKey], insertPos.lft);
|
|
363
|
+
const findCondition = scopeExpressions.length > 0
|
|
364
|
+
? and(lftCondition, ...scopeExpressions)
|
|
365
|
+
: lftCondition;
|
|
366
|
+
const findQuery = selectFrom(this.table).where(findCondition);
|
|
356
367
|
const { sql: findSql, params: findParams } = findQuery.compile(this.dialect);
|
|
357
368
|
const results = await this.executor.executeSql(findSql, findParams);
|
|
358
369
|
const rows = queryResultsToRows(results);
|
|
@@ -365,29 +376,56 @@ export class TreeManager<T extends TableDef> {
|
|
|
365
376
|
}
|
|
366
377
|
|
|
367
378
|
/**
|
|
368
|
-
* Removes a node
|
|
379
|
+
* Removes a node from its current tree position, promotes its direct children
|
|
380
|
+
* to the removed node's parent, and retains the removed row as a standalone root.
|
|
369
381
|
*/
|
|
370
382
|
async removeFromTree(node: TreeNodeResult): Promise<void> {
|
|
371
383
|
const nodeId = (node.data as Record<string, unknown>)[this.pkName];
|
|
384
|
+
const originalMaxRght = await this.getMaxRght();
|
|
372
385
|
|
|
386
|
+
// Promote direct children. Deeper descendants retain their immediate parent
|
|
387
|
+
// links, preserving the topology of every promoted child subtree.
|
|
373
388
|
await this.executeUpdate(
|
|
374
389
|
eq(this.table.columns[this.config.parentKey], nodeId),
|
|
375
390
|
{ [this.config.parentKey]: node.parentId }
|
|
376
391
|
);
|
|
377
392
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (gap.width > 2) {
|
|
383
|
-
await this.executeRawUpdate(
|
|
393
|
+
// Remove the node's left shell boundary from its strict descendants. This
|
|
394
|
+
// promotes the descendant forest one depth level while keeping nested subtrees.
|
|
395
|
+
if (node.rght - node.lft > 1) {
|
|
396
|
+
let sql =
|
|
384
397
|
`UPDATE ${this.quoteTable()} SET ` +
|
|
385
398
|
`${this.quoteCol(this.config.leftKey)} = ${this.quoteCol(this.config.leftKey)} - 1, ` +
|
|
386
|
-
`${this.quoteCol(this.config.rightKey)} = ${this.quoteCol(this.config.rightKey)} - 1
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
399
|
+
`${this.quoteCol(this.config.rightKey)} = ${this.quoteCol(this.config.rightKey)} - 1`;
|
|
400
|
+
|
|
401
|
+
if (this.config.depthKey) {
|
|
402
|
+
sql += `, ${this.quoteCol(this.config.depthKey)} = ${this.quoteCol(this.config.depthKey)} - 1`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
sql +=
|
|
406
|
+
` WHERE ${this.quoteCol(this.config.leftKey)} > ? AND ` +
|
|
407
|
+
`${this.quoteCol(this.config.rightKey)} < ?`;
|
|
408
|
+
|
|
409
|
+
await this.executeRawUpdate(sql, [node.lft, node.rght]);
|
|
390
410
|
}
|
|
411
|
+
|
|
412
|
+
// Remove the node's two shell slots from its former location. The node row
|
|
413
|
+
// itself is deliberately retained and repositioned after the compacted forest.
|
|
414
|
+
await this.shiftForDelete(node.rght, 2);
|
|
415
|
+
|
|
416
|
+
const detachedData: Record<string, unknown> = {
|
|
417
|
+
[this.config.parentKey]: null,
|
|
418
|
+
[this.config.leftKey]: originalMaxRght - 1,
|
|
419
|
+
[this.config.rightKey]: originalMaxRght,
|
|
420
|
+
};
|
|
421
|
+
if (this.config.depthKey) {
|
|
422
|
+
detachedData[this.config.depthKey] = 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await this.executeUpdate(
|
|
426
|
+
eq(this.table.columns[this.pkName], nodeId),
|
|
427
|
+
detachedData
|
|
428
|
+
);
|
|
391
429
|
}
|
|
392
430
|
|
|
393
431
|
/**
|
|
@@ -532,10 +570,24 @@ export class TreeManager<T extends TableDef> {
|
|
|
532
570
|
return 'id';
|
|
533
571
|
}
|
|
534
572
|
|
|
573
|
+
private getScopeEntries(): Array<[string, unknown]> {
|
|
574
|
+
return Object.entries(buildScopeConditions(this.config.scope, this.scopeValues));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private getScopeExpressions(): ReturnType<typeof eq>[] {
|
|
578
|
+
return this.getScopeEntries().map(([key, value]) =>
|
|
579
|
+
eq(this.table.columns[key], value)
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
535
583
|
private async getMaxRght(): Promise<number> {
|
|
536
584
|
const query = selectFrom(this.table)
|
|
537
585
|
.selectRaw(`MAX(${this.config.rightKey}) as max_rght`);
|
|
538
|
-
const
|
|
586
|
+
const scopeExpressions = this.getScopeExpressions();
|
|
587
|
+
const finalQuery = scopeExpressions.length > 0
|
|
588
|
+
? query.where(and(...scopeExpressions))
|
|
589
|
+
: query;
|
|
590
|
+
const { sql, params } = finalQuery.compile(this.dialect);
|
|
539
591
|
const queryResults = await this.executor.executeSql(sql, params);
|
|
540
592
|
const rows = queryResultsToRows(queryResults);
|
|
541
593
|
|
|
@@ -543,17 +595,17 @@ export class TreeManager<T extends TableDef> {
|
|
|
543
595
|
return typeof maxRght === 'number' ? maxRght : 0;
|
|
544
596
|
}
|
|
545
597
|
|
|
546
|
-
private async shiftForInsert(insertPoint: number): Promise<void> {
|
|
598
|
+
private async shiftForInsert(insertPoint: number, width: number = 2): Promise<void> {
|
|
547
599
|
await this.executeRawUpdate(
|
|
548
|
-
`UPDATE ${this.quoteTable()} SET ${this.quoteCol(this.config.rightKey)} = ${this.quoteCol(this.config.rightKey)} +
|
|
600
|
+
`UPDATE ${this.quoteTable()} SET ${this.quoteCol(this.config.rightKey)} = ${this.quoteCol(this.config.rightKey)} + ? ` +
|
|
549
601
|
`WHERE ${this.quoteCol(this.config.rightKey)} >= ?`,
|
|
550
|
-
[insertPoint]
|
|
602
|
+
[width, insertPoint]
|
|
551
603
|
);
|
|
552
604
|
|
|
553
605
|
await this.executeRawUpdate(
|
|
554
|
-
`UPDATE ${this.quoteTable()} SET ${this.quoteCol(this.config.leftKey)} = ${this.quoteCol(this.config.leftKey)} +
|
|
606
|
+
`UPDATE ${this.quoteTable()} SET ${this.quoteCol(this.config.leftKey)} = ${this.quoteCol(this.config.leftKey)} + ? ` +
|
|
555
607
|
`WHERE ${this.quoteCol(this.config.leftKey)} > ?`,
|
|
556
|
-
[insertPoint]
|
|
608
|
+
[width, insertPoint]
|
|
557
609
|
);
|
|
558
610
|
}
|
|
559
611
|
|
|
@@ -615,44 +667,45 @@ export class TreeManager<T extends TableDef> {
|
|
|
615
667
|
newDepth: number
|
|
616
668
|
): Promise<void> {
|
|
617
669
|
const width = NestedSetStrategy.subtreeWidth(node.lft, node.rght);
|
|
618
|
-
const
|
|
670
|
+
const oldDepth = this.config.depthKey
|
|
671
|
+
? (node.depth ?? await this.getLevel(node))
|
|
672
|
+
: 0;
|
|
619
673
|
const depthDelta = this.config.depthKey
|
|
620
|
-
? newDepth -
|
|
674
|
+
? newDepth - oldDepth
|
|
621
675
|
: 0;
|
|
622
676
|
const nodeId = (node.data as Record<string, unknown>)[this.pkName];
|
|
623
677
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
[tempOffset, node.lft, node.rght]
|
|
631
|
-
);
|
|
678
|
+
// Park the whole subtree below zero. The old positive temporary range was
|
|
679
|
+
// itself matched by shiftForDelete()/shiftForInsert(), so its boundaries
|
|
680
|
+
// moved before the restore step and the final delta became incorrect.
|
|
681
|
+
const isolateDelta = -10000000 - node.rght;
|
|
682
|
+
const isolatedLft = node.lft + isolateDelta;
|
|
683
|
+
const isolatedRght = node.rght + isolateDelta;
|
|
632
684
|
|
|
633
685
|
await this.executeRawUpdate(
|
|
634
686
|
`UPDATE ${this.quoteTable()} SET ` +
|
|
687
|
+
`${this.quoteCol(this.config.leftKey)} = ${this.quoteCol(this.config.leftKey)} + ?, ` +
|
|
635
688
|
`${this.quoteCol(this.config.rightKey)} = ${this.quoteCol(this.config.rightKey)} + ? ` +
|
|
636
689
|
`WHERE ${this.quoteCol(this.config.leftKey)} >= ? AND ${this.quoteCol(this.config.rightKey)} <= ?`,
|
|
637
|
-
[
|
|
690
|
+
[isolateDelta, isolateDelta, node.lft, node.rght]
|
|
638
691
|
);
|
|
639
692
|
|
|
640
693
|
await this.shiftForDelete(node.rght, width);
|
|
641
|
-
await this.shiftForInsert(newLft);
|
|
694
|
+
await this.shiftForInsert(newLft, width);
|
|
642
695
|
|
|
696
|
+
const restoreDelta = newLft - node.lft - isolateDelta;
|
|
643
697
|
let updateSql = `UPDATE ${this.quoteTable()} SET ` +
|
|
644
|
-
`${this.quoteCol(this.config.leftKey)} = ${this.quoteCol(this.config.leftKey)}
|
|
645
|
-
`${this.quoteCol(this.config.rightKey)} = ${this.quoteCol(this.config.rightKey)}
|
|
646
|
-
|
|
647
|
-
const updateParams: unknown[] = [tempOffset, delta, tempOffset, delta];
|
|
698
|
+
`${this.quoteCol(this.config.leftKey)} = ${this.quoteCol(this.config.leftKey)} + ?, ` +
|
|
699
|
+
`${this.quoteCol(this.config.rightKey)} = ${this.quoteCol(this.config.rightKey)} + ?`;
|
|
700
|
+
const updateParams: unknown[] = [restoreDelta, restoreDelta];
|
|
648
701
|
|
|
649
702
|
if (this.config.depthKey && depthDelta !== 0) {
|
|
650
703
|
updateSql += `, ${this.quoteCol(this.config.depthKey)} = ${this.quoteCol(this.config.depthKey)} + ?`;
|
|
651
704
|
updateParams.push(depthDelta);
|
|
652
705
|
}
|
|
653
706
|
|
|
654
|
-
updateSql += ` WHERE ${this.quoteCol(this.config.leftKey)} >= ?`;
|
|
655
|
-
updateParams.push(
|
|
707
|
+
updateSql += ` WHERE ${this.quoteCol(this.config.leftKey)} >= ? AND ${this.quoteCol(this.config.rightKey)} <= ?`;
|
|
708
|
+
updateParams.push(isolatedLft, isolatedRght);
|
|
656
709
|
|
|
657
710
|
await this.executeRawUpdate(updateSql, updateParams);
|
|
658
711
|
|
|
@@ -688,13 +741,28 @@ export class TreeManager<T extends TableDef> {
|
|
|
688
741
|
condition: ReturnType<typeof eq>,
|
|
689
742
|
data: Record<string, unknown>
|
|
690
743
|
): Promise<void> {
|
|
691
|
-
const
|
|
744
|
+
const scopeExpressions = this.getScopeExpressions();
|
|
745
|
+
const finalCondition = scopeExpressions.length > 0
|
|
746
|
+
? and(condition, ...scopeExpressions)
|
|
747
|
+
: condition;
|
|
748
|
+
const query = update(this.table).set(data).where(finalCondition);
|
|
692
749
|
const { sql, params } = query.compile(this.dialect);
|
|
693
750
|
await this.executor.executeSql(sql, params);
|
|
694
751
|
}
|
|
695
752
|
|
|
696
753
|
private async executeRawUpdate(sql: string, params: unknown[]): Promise<void> {
|
|
697
|
-
|
|
754
|
+
let scopedSql = sql;
|
|
755
|
+
const scopedParams = [...params];
|
|
756
|
+
let hasWhere = /\bWHERE\b/i.test(scopedSql);
|
|
757
|
+
|
|
758
|
+
for (const [key, value] of this.getScopeEntries()) {
|
|
759
|
+
scopedSql += hasWhere ? ' AND ' : ' WHERE ';
|
|
760
|
+
scopedSql += `${this.quoteCol(key)} = ?`;
|
|
761
|
+
scopedParams.push(value);
|
|
762
|
+
hasWhere = true;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
await this.executor.executeSql(scopedSql, scopedParams);
|
|
698
766
|
}
|
|
699
767
|
|
|
700
768
|
private quoteTable(): string {
|
|
@@ -751,4 +819,4 @@ function queryResultsToRows(results: QueryResult[]): Record<string, unknown>[] {
|
|
|
751
819
|
}
|
|
752
820
|
|
|
753
821
|
return rows;
|
|
754
|
-
}
|
|
822
|
+
}
|