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.
@@ -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
- await this.moveSubtree(node, newPos.lft, newParentId, newPos.depth);
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 unique lft value
354
- const findQuery = selectFrom(this.table)
355
- .where(eq(this.table.columns[this.config.leftKey], insertPos.lft));
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 and re-parents its children to the node's parent.
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
- const gap = NestedSetStrategy.calculateDeleteGap(node.lft, node.rght);
379
- await this.shiftForDelete(node.rght, 2);
380
-
381
- NestedSetStrategy.calculateShiftForDelete(node.lft + 1, gap.width - 2);
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
- `WHERE ${this.quoteCol(this.config.leftKey)} > ? AND ${this.quoteCol(this.config.rightKey)} < ?`,
388
- [node.lft, node.rght]
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 { sql, params } = query.compile(this.dialect);
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)} + 2 ` +
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)} + 2 ` +
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 delta = newLft - node.lft;
670
+ const oldDepth = this.config.depthKey
671
+ ? (node.depth ?? await this.getLevel(node))
672
+ : 0;
619
673
  const depthDelta = this.config.depthKey
620
- ? newDepth - (node.depth ?? 0)
674
+ ? newDepth - oldDepth
621
675
  : 0;
622
676
  const nodeId = (node.data as Record<string, unknown>)[this.pkName];
623
677
 
624
- const tempOffset = 10000000;
625
-
626
- await this.executeRawUpdate(
627
- `UPDATE ${this.quoteTable()} SET ` +
628
- `${this.quoteCol(this.config.leftKey)} = ${this.quoteCol(this.config.leftKey)} + ? ` +
629
- `WHERE ${this.quoteCol(this.config.leftKey)} >= ? AND ${this.quoteCol(this.config.rightKey)} <= ?`,
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
- [tempOffset, node.lft + tempOffset, node.rght]
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(node.lft + tempOffset);
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 query = update(this.table).set(data).where(condition);
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
- await this.executor.executeSql(sql, params);
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
+ }