@datarails-ai/dr-cli 1.6.116 → 1.6.118

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/cli.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@datarails-ai/dr-cli",
11
- version: "1.6.116",
11
+ version: "1.6.118",
12
12
  description: "Datarails CLI",
13
13
  type: "module",
14
14
  engines: {
@@ -69,7 +69,7 @@ var package_default = {
69
69
  };
70
70
 
71
71
  // src/buildInfo.ts
72
- var BUILD_TIME = "2026-10-12T07:26:22.083Z";
72
+ var BUILD_TIME = "2026-10-12T09:06:34.263Z";
73
73
  function formatBuildTime(iso) {
74
74
  if (iso === "") return "";
75
75
  const d = new Date(iso);
@@ -31273,6 +31273,61 @@ async function runPreviewData(cfg) {
31273
31273
  const result = await previewData(cfg.client, body, { signal: cfg.signal });
31274
31274
  writeJSONIndented(cfg.out, result);
31275
31275
  }
31276
+ async function findOrdinalCollisions(client, mapperId, docIds, signal) {
31277
+ const details = await getDetails2(client, mapperId, { signal });
31278
+ const tableId = details.template.id;
31279
+ const mappers = await list20(client, tableId, { signal });
31280
+ const self = mappers.find((m) => m.id === mapperId);
31281
+ if (self === void 0) {
31282
+ return null;
31283
+ }
31284
+ const ordinal = self.ordinal ?? null;
31285
+ const wanted = new Set(docIds);
31286
+ const others = mappers.filter(
31287
+ (m) => m.id !== mapperId && (m.ordinal ?? null) === ordinal && (m.mapped_documents_ids ?? []).some((d) => wanted.has(d))
31288
+ );
31289
+ return { tableId, ordinal, others };
31290
+ }
31291
+ async function warnOrdinalCollisions(cfg, body) {
31292
+ const err = cfg.err ?? process.stderr;
31293
+ const raw = body["selected_document_ids"];
31294
+ const docIds = (Array.isArray(raw) ? raw : []).map((d) => {
31295
+ const n = typeof d === "string" && /^\s*\d+\s*$/.test(d) ? Number(d) : d;
31296
+ if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
31297
+ throw new Error(
31298
+ `selected_document_ids must hold positive integer document ids, got ${JSON.stringify(d)}`
31299
+ );
31300
+ }
31301
+ return n;
31302
+ });
31303
+ if (docIds.length === 0) {
31304
+ return;
31305
+ }
31306
+ let found;
31307
+ try {
31308
+ found = await findOrdinalCollisions(cfg.client, cfg.mapperId, docIds, cfg.signal);
31309
+ } catch (e) {
31310
+ if (cfg.signal.aborted) {
31311
+ throw e;
31312
+ }
31313
+ err.write(
31314
+ `note: could not check for other mappers on the same ordinal: ${e.message}
31315
+ `
31316
+ );
31317
+ return;
31318
+ }
31319
+ if (found === null) {
31320
+ return;
31321
+ }
31322
+ const ordinal = found.ordinal === null ? "null" : String(found.ordinal);
31323
+ for (const other of found.others) {
31324
+ const shared = (other.mapped_documents_ids ?? []).filter((d) => docIds.includes(d));
31325
+ err.write(
31326
+ `warning: mapper ${cfg.mapperId} and mapper ${other.id} (${other.name}) share ordinal ${ordinal} on table ${found.tableId}, so the server treats them as versions of one mapper. Binding ${cfg.mapperId} to document(s) [${shared.join(", ")}] unbinds them from ${other.id}; a mapper left with no documents and no processed versions is deleted with its rows. To load both side by side, re-create one with \`dr datamappers create\` (a new mapper gets the next free ordinal).
31327
+ `
31328
+ );
31329
+ }
31330
+ }
31276
31331
  async function runUpdateBinding(cfg) {
31277
31332
  if (cfg.mapperId <= 0) {
31278
31333
  throw new Error("mapper id must be positive");
@@ -31297,6 +31352,7 @@ async function runUpdateBinding(cfg) {
31297
31352
  body["rescan"] = r;
31298
31353
  }
31299
31354
  }
31355
+ await warnOrdinalCollisions(cfg, body);
31300
31356
  const result = await updateBinding(cfg.client, cfg.mapperId, body, {
31301
31357
  signal: cfg.signal
31302
31358
  });
@@ -31456,6 +31512,18 @@ async function runDatamappersCreate(cfg) {
31456
31512
  "merge"
31457
31513
  );
31458
31514
  }
31515
+ const rawDatamapperId = body["datamapper_id"];
31516
+ const datamapperId = rawDatamapperId == null ? 0 : Number(rawDatamapperId);
31517
+ if (!Number.isInteger(datamapperId) || datamapperId < 0) {
31518
+ throw new Error(
31519
+ `body datamapper_id must be a mapper id (a non-negative integer), got ${JSON.stringify(rawDatamapperId)} \u2014 remove it to create a new mapper`
31520
+ );
31521
+ }
31522
+ const publishesVersion = datamapperId !== 0;
31523
+ const requestedOrdinal = body["ordinal"];
31524
+ if (!publishesVersion && requestedOrdinal == null) {
31525
+ body["ordinal"] = 0;
31526
+ }
31459
31527
  const { decoded: decodedCreate, changeCount: createChangeCount } = decodeBodyEntitiesDeep(body);
31460
31528
  if (createChangeCount > 0) {
31461
31529
  process.stderr.write(
@@ -31473,6 +31541,12 @@ async function runDatamappersCreate(cfg) {
31473
31541
  } else {
31474
31542
  renderMappersTable(cfg.out, [created]);
31475
31543
  }
31544
+ if (!publishesVersion && typeof requestedOrdinal === "number" && typeof created.ordinal === "number" && created.ordinal !== requestedOrdinal) {
31545
+ (cfg.err ?? process.stderr).write(
31546
+ `note: the server gave mapper ${created.id} ordinal ${created.ordinal}, not ${requestedOrdinal}: a new mapper always takes the next free ordinal on the table. To publish a new version of an existing mapper (same ordinal), use \`dr datamappers update\`.
31547
+ `
31548
+ );
31549
+ }
31476
31550
  const declaredCalcNames = extractCalcFields(body).map((c) => c.name).filter((n) => n !== "");
31477
31551
  if (declaredCalcNames.length > 0 && rescanValue !== "") {
31478
31552
  const result = await verifyCalcFieldsMaterialise(
@@ -32329,7 +32403,7 @@ function registerDatamappersCommand(root) {
32329
32403
  addServerFlag(previewCmd);
32330
32404
  addAccountFlag(previewCmd);
32331
32405
  const bindCmd = dm.command("bind").description(
32332
- 'Update which document IDs a mapper is bound to. Posts to `POST /api/templates/mappers/<id>/binding` with `{selected_document_ids, rescan?}`. The legacy webclient\'s "Mapping table with documents" modal calls this when the operator checks/unchecks docs in the binding picker.'
32406
+ 'Update which document IDs a mapper is bound to. Posts to `POST /api/templates/mappers/<id>/binding` with `{selected_document_ids, rescan?}`. The legacy webclient\'s "Mapping table with documents" modal calls this when the operator checks/unchecks docs in the binding picker. Warns first when another mapper of the table on the same ordinal is bound to one of those docs: mappers that share an ordinal are versions of one mapper, so the bind unbinds the other.'
32333
32407
  ).argument("<mapper_id>", "mapper id (positive integer)").option("--doc <id>", "Document id to bind (positive integer; repeatable)", collectInts, []).option(
32334
32408
  "--rescan <mode>",
32335
32409
  "Trigger a re-extraction: `latest` or `all` (default: omit; no rescan)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.116",
3
+ "cliVersion": "1.6.118",
4
4
  "_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
5
5
  "commands": {
6
6
  "login": {
@@ -61,7 +61,8 @@ operator's own org**. `dr integrations add` is the one exception: its payload na
61
61
  explicitly, so the **datasource lands in the right org while the filebox lands out of tenant**. Everything
62
62
  downstream then looks healthy — the binding resolves, the sync completes, a version lands — but it landed
63
63
  in the wrong org and the customer never sees the filebox. `dr connectors` (control-plane) accepts the
64
- impersonated JWT and takes its org from it as well, so `--org-id` can be omitted there. Beyond the
64
+ impersonated JWT and takes its org from it as well, so `--org-id` can be omitted there (an `--org-id`
65
+ for another org is refused 403). Beyond the
65
66
  breakage, creating customer resources under the operator's own identity breaks resource-level scoping and
66
67
  loses the audited operator→target trail (`tech/security.md` §2.2, §8).
67
68
 
@@ -758,9 +759,10 @@ POST gets `401`; the port-forward also serves the SPA). Use the dr-cli, which ca
758
759
  to the internal control-plane (`dr connectors connector create` / `query create` — added 2026-07;
759
760
  older skill copies show the now-broken curl):
760
761
  ```bash
761
- # a) Connector (creds source). --org-id optional — the JWT org is used when omitted, and under the
762
- # impersonated customer identity that IS the customer's org, so omitting it is correct. Note the returned id.
763
- dr connectors connector create --secret-ref <datasource_id> --json $ACC # add --org-id <org> only to override the JWT org
762
+ # a) Connector (creds source). Omit --org-id: the control plane creates the connector in the caller's
763
+ # JWT org, and under the impersonated customer identity that IS the customer's org. --org-id cannot
764
+ # override it: another org is refused 403, and a token with no org claim gets 400. Note the returned id.
765
+ dr connectors connector create --secret-ref <datasource_id> --json $ACC
764
766
  # b) ConnectorQuery (the binding) — connector-id from (a); filebox-uuid + filebox-id from step 3b
765
767
  # (filebox create). --filebox-id is the numeric doc id (MANDATORY; the Sync-query editor's write
766
768
  # key — no uuid->id resolver on the monolith). It's the same <filebox_id> used in steps 3a/5.
@@ -35,6 +35,21 @@ the matching subcommand. `datamappers create <flat|dimension|multiple|from-file>
35
35
  Required body fields: `name`, `document_version_id`, `selected_document_ids`
36
36
  (GET returns this renamed as `mapped_documents_ids`), `config`, `sheet_name`.
37
37
 
38
+ **Ordinal: mappers that share one are versions of one mapper.** Each mapper has
39
+ an `ordinal` (the ORDINAL column of `datamappers list --table <id>`). The server
40
+ binds documents per (table, ordinal): binding a mapper to a document moves that
41
+ document off any other mapper of the table on the same ordinal, and a mapper left
42
+ with no documents and no processed versions is then deleted with its rows. Two
43
+ different extractions from one filebox (for example two tabs of one workbook)
44
+ need two ordinals. `create` takes care of it: a new mapper always gets the
45
+ table's next free ordinal, which the server picks itself (any `ordinal` in a
46
+ `from-file` body is replaced, and dr notes the value it got). A body with no
47
+ ordinal, or a null one, is sent with `ordinal: 0` because the server would
48
+ otherwise store it as 0, a version of whichever mapper already holds 0.
49
+ A body with `datamapper_id` publishes a new version and keeps its ordinal as
50
+ sent; use `update` for that. `bind` warns, then binds anyway, when another
51
+ mapper of the table on the same ordinal is bound to one of the documents.
52
+
38
53
  **Schema-bootstrap shortcut:** mapping `map_to: "<field>"` to a column the target
39
54
  template doesn't have yet auto-adds that field on the table (plus 4 system
40
55
  scenario fields on the first mapper). This is the documented way to declare
@@ -87,7 +102,8 @@ value-inference behavior. `NAME` is the target **table field** name (the
87
102
  - `datamappers preview --payload-file <p>` — renders mapper output for sample
88
103
  input ("what would this extract?") before saving.
89
104
  - `datamappers bind <mapper_id> --doc <id> [--rescan latest|all]` — change which
90
- docs a mapper is bound to.
105
+ docs a mapper is bound to. A doc bound to another mapper of the table on the
106
+ same ordinal moves to this one (see *Ordinal* above), and dr warns first.
91
107
 
92
108
  ### `update --field NAME:TYPE` — set column types (materialising calc columns)
93
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-cli",
3
- "version": "1.6.116",
3
+ "version": "1.6.118",
4
4
  "description": "Datarails CLI",
5
5
  "type": "module",
6
6
  "engines": {