@objectstack/rest 12.5.0 → 13.0.0

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
@@ -1,5 +1,6 @@
1
1
  // src/rest-server.ts
2
2
  import { resolveAuthzContext, resolveLocalizationContext, isAuthGateAllowlisted } from "@objectstack/core";
3
+ import { isMcpServerEnabled } from "@objectstack/types";
3
4
 
4
5
  // src/route-manager.ts
5
6
  var RouteManager = class {
@@ -519,6 +520,17 @@ async function coerceRow(rawRow, metaMap, ctx) {
519
520
  }
520
521
 
521
522
  // src/import-runner.ts
523
+ import { bulkWrite, withTransientRetry } from "@objectstack/core";
524
+ function extractRecordId(rec) {
525
+ const id = rec?.id ?? rec?.record?.id;
526
+ return id != null ? String(id) : void 0;
527
+ }
528
+ function toFailedResult(rowNo, err) {
529
+ const code = err?.code ?? "IMPORT_ROW_FAILED";
530
+ const message = typeof err?.message === "string" ? err.message.slice(0, 300) : "Row failed";
531
+ return { row: rowNo, ok: false, action: "failed", error: message, code };
532
+ }
533
+ var MAX_CREATE_BATCH_SIZE = 200;
522
534
  function runImport(opts) {
523
535
  const {
524
536
  p,
@@ -606,7 +618,7 @@ function runImport(opts) {
606
618
  return recs[0];
607
619
  };
608
620
  const writeCtx = { ...context ?? {}, skipAutomations: !runAutomations };
609
- const results = [];
621
+ const results = new Array(rows.length);
610
622
  let okCount = 0, errCount = 0, created = 0, updated = 0, skipped = 0;
611
623
  let cancelled = false;
612
624
  const snapshot = (processed) => ({
@@ -617,6 +629,47 @@ function runImport(opts) {
617
629
  skipped,
618
630
  errors: errCount
619
631
  });
632
+ const canBulkCreate = typeof p.createManyData === "function";
633
+ const pendingCreates = [];
634
+ const flushPendingCreates = async () => {
635
+ if (pendingCreates.length === 0) return;
636
+ const batch = pendingCreates.splice(0, pendingCreates.length);
637
+ const writeResults = await bulkWrite(
638
+ batch.map((b) => b.data),
639
+ {
640
+ // Flush cadence follows progressEvery, but the write batch itself is
641
+ // capped independently — a caller-supplied progressEvery far above
642
+ // the issue's suggested 100-500 rows/batch must not translate into
643
+ // one oversized multi-row INSERT statement.
644
+ batchSize: Math.min(progressEvery, MAX_CREATE_BATCH_SIZE),
645
+ writeBatch: (chunk) => p.createManyData({
646
+ object: objectName,
647
+ records: chunk,
648
+ context: writeCtx,
649
+ ...environmentId ? { environmentId } : {}
650
+ }).then((r) => r.records),
651
+ writeOne: (row) => p.createData({
652
+ object: objectName,
653
+ data: row,
654
+ context: writeCtx,
655
+ ...environmentId ? { environmentId } : {}
656
+ })
657
+ }
658
+ );
659
+ for (const res of writeResults) {
660
+ const { index, rowNo } = batch[res.index];
661
+ if (res.ok) {
662
+ const id = extractRecordId(res.record);
663
+ okCount++;
664
+ created++;
665
+ if (collectUndo && id != null) undoLog.created.push(id);
666
+ results[index] = { row: rowNo, ok: true, action: "created", id };
667
+ } else {
668
+ errCount++;
669
+ results[index] = toFailedResult(rowNo, res.error);
670
+ }
671
+ }
672
+ };
620
673
  return (async () => {
621
674
  for (let i = 0; i < rows.length; i++) {
622
675
  const rowNo = i + 1;
@@ -630,7 +683,7 @@ function runImport(opts) {
630
683
  if (errors.length > 0) {
631
684
  const first2 = errors[0];
632
685
  errCount++;
633
- results.push({ row: rowNo, ok: false, action: "failed", field: first2.field, code: first2.code, error: first2.message });
686
+ results[i] = { row: rowNo, ok: false, action: "failed", field: first2.field, code: first2.code, error: first2.message };
634
687
  } else {
635
688
  let existing = "none";
636
689
  let handled = false;
@@ -638,11 +691,11 @@ function runImport(opts) {
638
691
  existing = await findExisting(data);
639
692
  if (existing === "ambiguous") {
640
693
  errCount++;
641
- results.push({ row: rowNo, ok: false, action: "failed", code: "AMBIGUOUS_MATCH", error: `matchFields matched more than one ${objectName} record` });
694
+ results[i] = { row: rowNo, ok: false, action: "failed", code: "AMBIGUOUS_MATCH", error: `matchFields matched more than one ${objectName} record` };
642
695
  handled = true;
643
696
  } else if (existing === "blank" && (skipBlankMatchKey || writeMode === "update")) {
644
697
  skipped++;
645
- results.push({ row: rowNo, ok: true, action: "skipped", code: "BLANK_MATCH_KEY" });
698
+ results[i] = { row: rowNo, ok: true, action: "skipped", code: "BLANK_MATCH_KEY" };
646
699
  handled = true;
647
700
  }
648
701
  }
@@ -651,45 +704,46 @@ function runImport(opts) {
651
704
  const willCreate = !willUpdate && (writeMode === "insert" || writeMode === "upsert");
652
705
  if (!willUpdate && !willCreate) {
653
706
  skipped++;
654
- results.push({ row: rowNo, ok: true, action: "skipped", code: "NO_MATCH" });
707
+ results[i] = { row: rowNo, ok: true, action: "skipped", code: "NO_MATCH" };
655
708
  } else if (dryRun) {
656
709
  okCount++;
657
710
  if (willUpdate) {
658
711
  updated++;
659
- results.push({ row: rowNo, ok: true, action: "updated", id: String(existing.id ?? "") || void 0 });
712
+ results[i] = { row: rowNo, ok: true, action: "updated", id: String(existing.id ?? "") || void 0 };
660
713
  } else {
661
714
  created++;
662
- results.push({ row: rowNo, ok: true, action: "created" });
715
+ results[i] = { row: rowNo, ok: true, action: "created" };
663
716
  }
664
717
  } else if (willUpdate) {
665
718
  const target = existing;
666
- const res2 = await p.updateData({ object: objectName, id: target.id, data, context: writeCtx, ...environmentId ? { environmentId } : {} });
667
- const id = res2?.id ?? res2?.record?.id ?? target.id;
719
+ const res2 = await withTransientRetry(() => p.updateData({ object: objectName, id: target.id, data, context: writeCtx, ...environmentId ? { environmentId } : {} }));
720
+ const id = extractRecordId(res2) ?? String(target.id);
668
721
  okCount++;
669
722
  updated++;
670
723
  if (collectUndo && target.id != null) {
671
724
  undoLog.updated.push({ id: String(target.id), before: captureBefore(target, data) });
672
725
  }
673
- results.push({ row: rowNo, ok: true, action: "updated", id: id != null ? String(id) : void 0 });
726
+ results[i] = { row: rowNo, ok: true, action: "updated", id };
727
+ } else if (canBulkCreate) {
728
+ pendingCreates.push({ index: i, rowNo, data });
674
729
  } else {
675
730
  const res2 = await p.createData({ object: objectName, data, context: writeCtx, ...environmentId ? { environmentId } : {} });
676
- const id = res2?.id ?? res2?.record?.id;
731
+ const id = extractRecordId(res2);
677
732
  okCount++;
678
733
  created++;
679
- if (collectUndo && id != null) undoLog.created.push(String(id));
680
- results.push({ row: rowNo, ok: true, action: "created", id: id != null ? String(id) : void 0 });
734
+ if (collectUndo && id != null) undoLog.created.push(id);
735
+ results[i] = { row: rowNo, ok: true, action: "created", id };
681
736
  }
682
737
  }
683
738
  }
684
739
  } catch (err) {
685
740
  errCount++;
686
- const code = err?.code ?? "IMPORT_ROW_FAILED";
687
- const message = typeof err?.message === "string" ? err.message.slice(0, 300) : "Row failed";
688
- results.push({ row: rowNo, ok: false, action: "failed", error: message, code });
741
+ results[i] = toFailedResult(rowNo, err);
689
742
  }
690
743
  const processed = i + 1;
691
- if (onProgress && (processed % progressEvery === 0 || processed === rows.length)) {
692
- await onProgress(snapshot(processed));
744
+ if (processed % progressEvery === 0 || processed === rows.length) {
745
+ await flushPendingCreates();
746
+ if (onProgress) await onProgress(snapshot(processed));
693
747
  }
694
748
  if (shouldCancel && processed < rows.length && processed % progressEvery === 0) {
695
749
  if (await shouldCancel()) {
@@ -698,10 +752,12 @@ function runImport(opts) {
698
752
  }
699
753
  }
700
754
  }
755
+ await flushPendingCreates();
756
+ const compacted = results.filter((r) => r !== void 0);
701
757
  return {
702
- ...snapshot(results.length),
758
+ ...snapshot(compacted.length),
703
759
  ok: okCount,
704
- results,
760
+ results: compacted,
705
761
  cancelled,
706
762
  ...collectUndo ? { undoLog } : {}
707
763
  };
@@ -1698,7 +1754,7 @@ var RestServer = class {
1698
1754
  userId: authz.userId,
1699
1755
  tenantId: authz.tenantId,
1700
1756
  email: authz.email,
1701
- roles: authz.roles,
1757
+ positions: authz.positions,
1702
1758
  permissions: authz.permissions,
1703
1759
  systemPermissions: authz.systemPermissions,
1704
1760
  ...authz.tabPermissions ? { tabPermissions: authz.tabPermissions } : {},
@@ -2156,8 +2212,7 @@ var RestServer = class {
2156
2212
  if (this.config.api.enableUi) {
2157
2213
  discovery.routes.ui = `${realBase}/ui`;
2158
2214
  }
2159
- const mcpEnabled = globalThis?.process?.env?.OS_MCP_SERVER_ENABLED === "true";
2160
- if (mcpEnabled) {
2215
+ if (isMcpServerEnabled()) {
2161
2216
  const unscopedBase = isScoped ? basePath.replace(/\/(environments|projects)\/:environmentId$/, "") : basePath;
2162
2217
  discovery.routes.mcp = `${unscopedBase}/mcp`;
2163
2218
  } else {