@versionone/agility-sdk 1.0.0-alpha.2 → 1.0.0-alpha.4

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 CHANGED
@@ -35,19 +35,25 @@ __export(index_exports, {
35
35
  TypeCache: () => TypeCache,
36
36
  UNGUARDABLE_TYPES: () => UNGUARDABLE_TYPES,
37
37
  UnguardableError: () => UnguardableError,
38
+ UsageError: () => UsageError,
39
+ WrappedError: () => WrappedError,
38
40
  assetApi: () => assetApi_exports,
39
41
  assetStateOf: () => assetStateOf,
40
42
  attachment: () => attachment_exports,
41
43
  createFetchTransport: () => createFetchTransport,
42
44
  describeBatch: () => describe,
45
+ fields: () => fields_exports,
43
46
  guard: () => guard,
47
+ importantName: () => importantName,
44
48
  isAssetNumber: () => isAssetNumber,
45
49
  isOidToken: () => isOidToken,
50
+ kindOf: () => kindOf,
46
51
  loc2: () => loc2_exports,
47
52
  meta: () => meta_exports,
48
53
  momentless: () => momentless,
49
54
  namesOf: () => namesOf,
50
55
  parseOid: () => parseOid,
56
+ parseViolation: () => parseViolation,
51
57
  queryV1: () => queryV1_exports,
52
58
  resolveNumbers: () => resolveNumbers,
53
59
  resolveRef: () => resolveRef,
@@ -67,16 +73,61 @@ __export(index_exports, {
67
73
  module.exports = __toCommonJS(index_exports);
68
74
 
69
75
  // src/core/errors.ts
76
+ function kindOf(status, code) {
77
+ switch (code) {
78
+ case "ABORTED":
79
+ return "aborted";
80
+ case "TIMEOUT":
81
+ case "NETWORK_ERROR":
82
+ return "transient";
83
+ case "BAD_JSON":
84
+ case "BAD_RESPONSE":
85
+ return "protocol";
86
+ case "USAGE":
87
+ return "usage";
88
+ case "CommandFailure":
89
+ return "commandFailure";
90
+ case "QUERY_REJECTED":
91
+ case "TOO_LARGE":
92
+ return "invalid";
93
+ case "NOT_FOUND":
94
+ return "notFound";
95
+ }
96
+ if (status === 401) return "auth";
97
+ if (status === 403) return "forbidden";
98
+ if (status === 404) return "notFound";
99
+ if (status === 408 || status === 429 || status >= 500) return "transient";
100
+ if (status >= 400) return "invalid";
101
+ return "unknown";
102
+ }
103
+ var causeOf = (details) => details && typeof details === "object" && "cause" in details ? details.cause : void 0;
70
104
  var ApiError = class extends Error {
105
+ /** The HTTP status; 0 when no answer came back (network, timeout, abort, usage). */
71
106
  status;
72
107
  code;
73
108
  details;
74
- constructor(message, status, code, details) {
75
- super(message);
109
+ kind;
110
+ constructor(message, status, code, details, kind = kindOf(status, code)) {
111
+ const cause = causeOf(details);
112
+ super(message, cause === void 0 ? void 0 : { cause });
76
113
  this.name = "ApiError";
77
114
  this.status = status;
78
115
  this.code = code;
79
116
  this.details = details;
117
+ this.kind = kind;
118
+ }
119
+ };
120
+ var UsageError = class extends ApiError {
121
+ constructor(message, details) {
122
+ super(message, 0, "USAGE", details, "usage");
123
+ this.name = "UsageError";
124
+ }
125
+ };
126
+ var WrappedError = class extends ApiError {
127
+ constructor(message, code, cause, details) {
128
+ const inner = cause instanceof ApiError ? cause : void 0;
129
+ super(message, inner?.status ?? 0, code, { ...details, cause }, inner?.kind ?? "unknown");
130
+ this.name = "WrappedError";
80
131
  }
81
132
  };
82
133
 
@@ -216,7 +267,7 @@ function isOidToken(value) {
216
267
  }
217
268
  function parseOid(token) {
218
269
  const m = OID_TOKEN.exec(token);
219
- if (!m) throw new Error(`"${token}" is not an OID token (Story:1234)`);
270
+ if (!m) throw new UsageError(`"${token}" is not an OID token (Story:1234)`);
220
271
  return { type: m[1], id: Number(m[2]), moment: m[3] === void 0 ? null : Number(m[3]) };
221
272
  }
222
273
  function momentless(token) {
@@ -240,6 +291,7 @@ __export(assetApi_exports, {
240
291
  mergeOutcome: () => mergeOutcome,
241
292
  momentless: () => momentless,
242
293
  outcomeOf: () => outcomeOf,
294
+ parseViolation: () => parseViolation,
243
295
  preview: () => preview,
244
296
  queries: () => queries,
245
297
  query: () => query,
@@ -405,15 +457,25 @@ function toNumber(value) {
405
457
  const n = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
406
458
  return Number.isNaN(n) ? value : n;
407
459
  }
460
+ function parseViolation(message) {
461
+ const parts = message.trim().split("'");
462
+ if (parts[0] !== "Violation" || !parts[1]) return null;
463
+ const rule = parts[1], rest = parts.slice(2).filter((p) => p !== "");
464
+ if (rest[0] === "AttributeDefinition" && rest[1] && rest[2]) return { rule, attribute: `${rest[2]}.${rest[1]}`, tag: rest.join("'") };
465
+ const token = rest.length === 1 && /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*$/.test(rest[0]) ? rest[0] : null;
466
+ return { rule, attribute: token, tag: !token && rest.length > 1 ? rest.join("'") : null };
467
+ }
408
468
  function failuresOf(response) {
409
469
  return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => ({
410
470
  index: typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
411
471
  message: typeof error === "string" ? error : error?.message || "command failed",
412
- echoed
472
+ echoed,
473
+ violation: parseViolation(typeof error === "string" ? error : error?.message || "")
413
474
  }));
414
475
  }
415
476
  function outcomeOf(response) {
416
477
  return {
478
+ requestId: typeof response.requestId === "string" && response.requestId ? response.requestId : null,
417
479
  created: (response.assetsCreated?.oidTokens ?? []).map(momentless),
418
480
  modified: (response.assetsModified?.oidTokens ?? []).map(momentless),
419
481
  operated: (response.assetsOperatedOn?.oidTokens ?? []).map(momentless),
@@ -422,24 +484,28 @@ function outcomeOf(response) {
422
484
  durationSeconds: typeof response.durationSeconds === "number" ? response.durationSeconds : null
423
485
  };
424
486
  }
425
- var emptyOutcome = () => ({ created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
487
+ var emptyOutcome = () => ({ requestId: null, created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
426
488
  function mergeOutcome(into, done, offset) {
427
489
  into.created.push(...done.created);
428
490
  into.modified.push(...done.modified);
429
491
  into.operated.push(...done.operated);
430
492
  into.failures.push(...done.failures.map((f) => ({ ...f, index: f.index === null ? null : f.index + offset })));
431
493
  into.queryResults.push(...done.queryResults);
494
+ into.requestId = null;
432
495
  if (done.durationSeconds !== null) into.durationSeconds = (into.durationSeconds ?? 0) + done.durationSeconds;
433
496
  return into;
434
497
  }
435
498
  var CommandFailureError = class extends ApiError {
436
499
  failures;
437
500
  response;
501
+ /** What the batch did, in the model's terms: the commands that succeeded are committed. */
502
+ outcome;
438
503
  constructor(response, failures = failuresOf(response)) {
439
504
  super(failures.map((f) => f.message).join("; ") || "command failed", 200, "CommandFailure", failures);
440
505
  this.name = "CommandFailureError";
441
506
  this.failures = failures;
442
507
  this.response = response;
508
+ this.outcome = outcomeOf(response);
443
509
  }
444
510
  };
445
511
  var bodyOf = (c) => {
@@ -475,7 +541,7 @@ async function send(transport, commands, options = {}) {
475
541
  const results = response.queryResult?.results ?? [];
476
542
  let aligned = realign(queries3, results, failed);
477
543
  if (!aligned) {
478
- if (!failures.some((f) => f.index === null)) throw new Error(`api/asset returned ${results.length} query results for ${queries3.filter((q) => !failed.has(q.index)).length} queries that did not fail; cannot tell which result belongs to which query`);
544
+ if (!failures.some((f) => f.index === null)) throw new ApiError(`api/asset returned ${results.length} query results for ${queries3.filter((q) => !failed.has(q.index)).length} queries that did not fail; cannot tell which result belongs to which query`, 200, "BAD_RESPONSE", response);
479
545
  aligned = queries3.map(() => null);
480
546
  }
481
547
  const contextOf = (i) => {
@@ -497,7 +563,7 @@ async function query(transport, q, options = {}) {
497
563
  return rows ?? [];
498
564
  }
499
565
  async function queryPage(transport, q, options = {}) {
500
- if (!q.page) throw new Error(`query of ${q.from} without a page: every api/asset read must be bounded`);
566
+ if (!q.page) throw new UsageError(`query of ${q.from} without a page: every api/asset read must be bounded`);
501
567
  const response = await send(transport, [{ ...q, needTotal: true }], { ...options, throwOnFailure: true });
502
568
  const rows = response.queryResult.results[0] ?? [];
503
569
  const reported = response.queryResult.resultMetaData?.[0]?.total;
@@ -505,7 +571,7 @@ async function queryPage(transport, q, options = {}) {
505
571
  return { rows, total, more: total === null ? rows.length === q.page.size : q.page.start + rows.length < total };
506
572
  }
507
573
  async function queries(transport, qs, options = {}) {
508
- for (const q of qs) if (!q.page) throw new Error(`query of ${q.from} without a page: every api/asset read must be bounded`);
574
+ for (const q of qs) if (!q.page) throw new UsageError(`query of ${q.from} without a page: every api/asset read must be bounded`);
509
575
  const response = await send(transport, qs, { ...options, throwOnFailure: true });
510
576
  return qs.map((_, i) => response.queryResult.results[i] ?? []);
511
577
  }
@@ -543,7 +609,7 @@ function fromBase64(text) {
543
609
  }
544
610
  function numericId(spec) {
545
611
  const m = /^(?:[A-Za-z]+:)?(\d+)(?::\d+)?$/.exec(String(spec).trim());
546
- if (!m) throw new TypeError(`"${spec}" is not an asset id (5678, or an OID token such as Attachment:5678)`);
612
+ if (!m) throw new UsageError(`"${spec}" is not an asset id (5678, or an OID token such as Attachment:5678)`);
547
613
  return m[1];
548
614
  }
549
615
  var join = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path}`;
@@ -639,9 +705,9 @@ async function get(transport, id) {
639
705
  const file = await getFile(transport, meta.id);
640
706
  return { ...meta, bytes: file.bytes, contentType: file.contentType ?? meta.contentType, recordedContentType: meta.contentType };
641
707
  }
642
- var AttachmentUploadError = class extends Error {
708
+ var AttachmentUploadError = class extends WrappedError {
643
709
  constructor(attachment, cause) {
644
- super(`${attachment.oid} was created on its asset with no content, and uploading the file failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
710
+ super(`${attachment.oid} was created on its asset with no content, and uploading the file failed: ${cause instanceof Error ? cause.message : String(cause)}`, "ATTACHMENT_UPLOAD", cause);
645
711
  this.attachment = attachment;
646
712
  this.name = "AttachmentUploadError";
647
713
  }
@@ -674,7 +740,7 @@ async function remove(transport, id) {
674
740
  // src/services/guard.ts
675
741
  var CHUNK = 100;
676
742
  var UNGUARDABLE_TYPES = ["Idea", "IdeaChangeSet", "IdeaStatus", "IdeaComment", "IdeaCategory", "IdeaForum", "IdeaUser"];
677
- var UnguardableError = class extends Error {
743
+ var UnguardableError = class extends UsageError {
678
744
  reasons;
679
745
  constructor(reasons) {
680
746
  super(`cannot guard this batch: ${reasons.map((r) => `command ${r.index}: ${r.reason}`).join("; ")}`);
@@ -740,7 +806,7 @@ function guard(projected, stamps) {
740
806
  unstamped.push(x.oid);
741
807
  continue;
742
808
  }
743
- if (!STAMP.test(stamp)) throw new Error(`${x.oid}: ChangeDate ${JSON.stringify(stamp)} is not a timestamp; refusing to put it in a filter`);
809
+ if (!STAMP.test(stamp)) throw new ApiError(`${x.oid}: ChangeDate ${JSON.stringify(stamp)} is not a timestamp; refusing to put it in a filter`, 200, "BAD_RESPONSE", { oid: x.oid, stamp });
744
810
  const scope = { from: typeOfOid(x.oid), filter: ["ID=$oid", `ChangeDate='${stamp}'`], with: { $oid: x.oid } };
745
811
  commands.push(x.update ? { ...scope, update: x.update } : { ...scope, execute: x.execute });
746
812
  }
@@ -757,16 +823,14 @@ function passesOf(projected) {
757
823
  }
758
824
  return passes;
759
825
  }
760
- var GuardedPassError = class extends Error {
826
+ var GuardedPassError = class extends WrappedError {
761
827
  constructor(cause, outcome, sent, pass) {
762
- super(`guarded send failed in pass ${pass + 1}${pass ? " (earlier passes applied)" : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
763
- this.cause = cause;
828
+ super(`guarded send failed in pass ${pass + 1}${pass ? " (earlier passes applied)" : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, "GUARDED_PASS", cause);
764
829
  this.outcome = outcome;
765
830
  this.sent = sent;
766
831
  this.pass = pass;
767
832
  this.name = "GuardedPassError";
768
833
  }
769
- cause;
770
834
  outcome;
771
835
  sent;
772
836
  pass;
@@ -983,18 +1047,18 @@ function reverseOf(described, before, done, context = {}) {
983
1047
  if (commands.length) notes.unshift("restores the state recorded at snapshotAt; anything that changed between then and sentAt is not reflected, and a scalar restored to null is cleared, not unset");
984
1048
  return { commands, notes };
985
1049
  }
986
- var BatchRecordError = class extends Error {
1050
+ var BatchRecordError = class extends WrappedError {
987
1051
  receipt;
988
1052
  constructor(cause, receipt) {
989
- super(`the batch was sent and its outcome recorded, but reading the assets back afterwards failed, so receipt.after is incomplete: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
1053
+ super(`the batch was sent and its outcome recorded, but reading the assets back afterwards failed, so receipt.after is incomplete: ${cause instanceof Error ? cause.message : String(cause)}`, "BATCH_RECORD", cause);
990
1054
  this.name = "BatchRecordError";
991
1055
  this.receipt = receipt;
992
1056
  }
993
1057
  };
994
- var BatchSendError = class extends Error {
1058
+ var BatchSendError = class extends WrappedError {
995
1059
  receipt;
996
1060
  constructor(cause, receipt) {
997
- super(`the batch send failed; whether the server applied it is unknown: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
1061
+ super(`the batch send failed; whether the server applied it is unknown: ${cause instanceof Error ? cause.message : String(cause)}`, "BATCH_SEND", cause);
998
1062
  this.name = "BatchSendError";
999
1063
  this.receipt = receipt;
1000
1064
  }
@@ -1002,7 +1066,7 @@ var BatchSendError = class extends Error {
1002
1066
  var now = () => (/* @__PURE__ */ new Date()).toISOString();
1003
1067
  async function runBatch(transport, schema, commands, options = {}) {
1004
1068
  const useGuard = options.guard ?? true;
1005
- if (!commands.length) throw new Error("the batch is empty");
1069
+ if (!commands.length) throw new UsageError("the batch is empty");
1006
1070
  const refused = unguardable(commands);
1007
1071
  const fatal = refused.filter((r) => r.reason.includes("rejected by the server"));
1008
1072
  if (fatal.length || useGuard && refused.length) throw new UnguardableError(useGuard ? refused : fatal);
@@ -1108,6 +1172,65 @@ async function reversible(transport, receipt, options = {}) {
1108
1172
  return { commands, drifted, reactivated };
1109
1173
  }
1110
1174
 
1175
+ // src/wire/fields.ts
1176
+ var fields_exports = {};
1177
+ __export(fields_exports, {
1178
+ visibleFields: () => visibleFields,
1179
+ visibleFieldsByAsset: () => visibleFieldsByAsset
1180
+ });
1181
+ async function visibleFields(transport, types, scopes) {
1182
+ if (!types.length) return {};
1183
+ const params = [...scopes.map((s) => `scopeOid=${encodeURIComponent(momentless(s))}`), ...types.map((t) => `assetTypeToken=${encodeURIComponent(t)}`)].join("&");
1184
+ const answer = await transport.request({ method: "GET", path: `/api/visibleFields?${params}`, responseType: "json" });
1185
+ if (!Array.isArray(answer)) throw new ApiError("api/visibleFields did not answer a list", 200, "BAD_RESPONSE", answer);
1186
+ const out = {};
1187
+ for (const entry of answer) if (entry && typeof entry.assetType === "string") out[entry.assetType] = Array.isArray(entry.fields) ? entry.fields.filter((f) => typeof f === "string") : [];
1188
+ return out;
1189
+ }
1190
+ async function visibleFieldsByAsset(transport, oid) {
1191
+ const answer = await transport.request({ method: "GET", path: "/api/visibleFieldsByAsset", query: { oidToken: momentless(oid) }, responseType: "json" });
1192
+ if (!Array.isArray(answer)) throw new ApiError("api/visibleFieldsByAsset did not answer a list", 200, "BAD_RESPONSE", answer);
1193
+ return answer.filter((f) => typeof f === "string");
1194
+ }
1195
+
1196
+ // src/services/fields.ts
1197
+ async function lineage(schema, type) {
1198
+ const out = [];
1199
+ for (let t = type; t && !out.includes(t); ) {
1200
+ out.push(t);
1201
+ const def = await schema.load(t);
1202
+ t = def?.Base;
1203
+ }
1204
+ return out;
1205
+ }
1206
+ function importantName(token, types) {
1207
+ const dot = token.indexOf(".");
1208
+ if (dot < 0) return token;
1209
+ return types.includes(token.slice(0, dot)) ? token.slice(dot + 1) : null;
1210
+ }
1211
+ async function describe2(transport, schema, type, scope) {
1212
+ const scopeOid = momentless(scope);
1213
+ const types = await lineage(schema, type);
1214
+ const definition = await schema.load(type);
1215
+ const required = definition ? Object.values(definition.Attributes).filter((a) => a.IsRequired && !a.IsReadOnly).map((a) => a.Name).sort() : [];
1216
+ const [visible, [row]] = await Promise.all([
1217
+ visibleFields(transport, [type], [scopeOid]),
1218
+ query(transport, { from: "Scope", select: [{ from: "Scheme", select: ["ImportantFields", "IsRequiredAtClose"] }], filter: ["ID=$scope"], with: { $scope: scopeOid }, page: { size: 1, start: 0 } }, { lookup: schema })
1219
+ ]);
1220
+ const scheme = row?.Scheme ?? null;
1221
+ const tokens = Array.isArray(scheme?.ImportantFields) ? scheme.ImportantFields.filter((t) => typeof t === "string") : [];
1222
+ const important = [...new Set(tokens.map((t) => importantName(t, types)).filter((n) => !!n))].sort();
1223
+ return {
1224
+ type,
1225
+ scope: scopeOid,
1226
+ scheme: scheme ? momentless(scheme._oid) : null,
1227
+ required,
1228
+ visible: visible[type] ?? [],
1229
+ important,
1230
+ requiredAtClose: scheme?.IsRequiredAtClose === true
1231
+ };
1232
+ }
1233
+
1111
1234
  // src/types/asset.ts
1112
1235
  var ASSET_STATE_CODES = { Future: 0, Active: 64, Closed: 128, Dead: 192, Template: 200, OldBrokenDownStory: 208, Deprecated: 254, Deleted: 255 };
1113
1236
  var STATE_OF_CODE = Object.fromEntries(Object.entries(ASSET_STATE_CODES).map(([name, code]) => [code, name]));
@@ -1173,7 +1296,7 @@ async function resolveNumbers(transport, numbers, options = {}) {
1173
1296
  for (const n of numbers) if (!firsts.has(n.trim().toUpperCase())) firsts.set(n.trim().toUpperCase(), n.trim());
1174
1297
  const asked = [...firsts.values()];
1175
1298
  const invalid = asked.filter((n) => !isAssetNumber(n));
1176
- if (invalid.length) throw new TypeError(`not asset numbers (expected a prefix and digits, S-1234): ${invalid.join(", ")}`);
1299
+ if (invalid.length) throw new UsageError(`not asset numbers (expected a prefix and digits, S-1234): ${invalid.join(", ")}`);
1177
1300
  const byNumber = /* @__PURE__ */ new Map();
1178
1301
  for (let i = 0; i < asked.length; i += CHUNK3) {
1179
1302
  for (const r of await askRoots(transport, asked.slice(i, i + CHUNK3), options.types)) {
@@ -1194,7 +1317,7 @@ async function resolveRef(transport, spec, options = {}) {
1194
1317
  return { number: null, oid, type: typeOfOid(oid), name: null, assetState: null };
1195
1318
  }
1196
1319
  if (isAssetNumber(text)) return (await resolveNumbers(transport, [text], options)).found[0] ?? null;
1197
- throw new TypeError(`"${spec}" is neither an asset number (S-1234) nor an OID token (Story:1234)`);
1320
+ throw new UsageError(`"${spec}" is neither an asset number (S-1234) nor an OID token (Story:1234)`);
1198
1321
  }
1199
1322
 
1200
1323
  // src/wire/meta.ts
@@ -1256,7 +1379,7 @@ async function getAssetType(transport, token) {
1256
1379
  }
1257
1380
  async function getAttributeDefinition(transport, tokenref) {
1258
1381
  const [type, attribute] = tokenref.split(".");
1259
- if (!type || !attribute) throw new Error(`"${tokenref}" is not an attribute token ("Story.Name")`);
1382
+ if (!type || !attribute) throw new UsageError(`"${tokenref}" is not an attribute token ("Story.Name")`);
1260
1383
  return toAttributeDefinition(await transport.request({ method: "GET", path: `/meta.v1/${encodeURIComponent(type)}/${encodeURIComponent(attribute)}`, responseType: "json" }));
1261
1384
  }
1262
1385
 
@@ -1383,7 +1506,7 @@ async function localize(transport, tags, options = {}) {
1383
1506
  const encoded = encodeURIComponent(toLson(unique));
1384
1507
  const headers = options.culture ? { "Accept-Language": options.culture } : void 0;
1385
1508
  const result = encoded.length <= (options.maxGetLength ?? DEFAULT_MAX_GET_LENGTH) ? await transport.request({ method: "GET", path: `/loc-2.v1?${encoded}`, headers, responseType: "json" }) : await transport.request({ method: "POST", path: "/loc-2.v1", headers, body: encoded, contentType: "application/x-www-form-urlencoded", responseType: "json", idempotent: true });
1386
- if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error(`loc-2.v1 answered an array of tags with ${JSON.stringify(result)?.slice(0, 100)}, not an object`);
1509
+ if (!result || typeof result !== "object" || Array.isArray(result)) throw new ApiError(`loc-2.v1 answered an array of tags with ${JSON.stringify(result)?.slice(0, 100)}, not an object`, 200, "BAD_RESPONSE", result);
1387
1510
  return result;
1388
1511
  }
1389
1512
  async function localizeOne(transport, tag, options = {}) {
@@ -1403,7 +1526,7 @@ function rejected(error) {
1403
1526
  return messages.length ? new ApiError(messages.join("; "), error.status, "QUERY_REJECTED", error.details) : error;
1404
1527
  }
1405
1528
  async function queries2(transport, qs, options = {}) {
1406
- for (const q of qs) if (!q.page) throw new Error(`query of ${q.from} without a page: every query.v1 read must be bounded`);
1529
+ for (const q of qs) if (!q.page) throw new UsageError(`query of ${q.from} without a page: every query.v1 read must be bounded`);
1407
1530
  if (!qs.length) return [];
1408
1531
  let response;
1409
1532
  try {
@@ -1433,6 +1556,8 @@ __export(rest1_exports, {
1433
1556
  history: () => history,
1434
1557
  list: () => list2,
1435
1558
  momentless: () => momentless,
1559
+ operate: () => operate,
1560
+ template: () => template,
1436
1561
  toQuery: () => toQuery
1437
1562
  });
1438
1563
  var toRef = (doc) => ({ _oid: momentless(doc.idref) });
@@ -1464,8 +1589,8 @@ function fromAssetDocument(doc, { history: history2 = false } = {}) {
1464
1589
  }
1465
1590
  function toQuery(params) {
1466
1591
  return {
1467
- sel: params.sel?.join(","),
1468
- where: params.where?.length ? params.where.join(";") : void 0,
1592
+ sel: (params.select ?? params.sel)?.join(","),
1593
+ where: (params.filter ?? params.where)?.length ? (params.filter ?? params.where).join(";") : void 0,
1469
1594
  with: params.with ? Object.entries(params.with).map(([k, v]) => `${k}=${[].concat(v).join(",")}`).join("|") : void 0,
1470
1595
  find: params.find,
1471
1596
  findin: params.findin?.join(","),
@@ -1477,16 +1602,29 @@ function toQuery(params) {
1477
1602
  };
1478
1603
  }
1479
1604
  async function read(transport, path, params, history2) {
1480
- if (!params.page) throw new Error(`rest-1 read of ${path} without a page: every read must be bounded`);
1605
+ if (!params.page) throw new UsageError(`rest-1 read of ${path} without a page: every read must be bounded`);
1481
1606
  const doc = await transport.request({ method: "GET", path, query: toQuery(params), responseType: "json" });
1482
1607
  return { rows: (doc.Assets ?? []).map((a) => fromAssetDocument(a, { history: history2 })), total: typeof doc.total === "number" && doc.total >= 0 ? doc.total : null, pageSize: doc.pageSize, pageStart: doc.pageStart };
1483
1608
  }
1484
1609
  var list2 = (transport, type, params) => read(transport, `/rest-1.v1/Data/${encodeURIComponent(type)}`, params, false);
1485
- async function get2(transport, oid, sel) {
1610
+ async function get2(transport, oid, select) {
1486
1611
  const [type, id] = momentless(oid).split(":");
1487
- const doc = await transport.request({ method: "GET", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { sel: sel?.join(",") }, responseType: "json" });
1612
+ const doc = await transport.request({ method: "GET", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { sel: select?.join(",") }, responseType: "json" });
1488
1613
  return fromAssetDocument(doc);
1489
1614
  }
1615
+ async function operate(transport, oid, op) {
1616
+ const [type, id] = momentless(oid).split(":");
1617
+ if (!type || !id) throw new UsageError(`"${oid}" is not an OID token (Story:1234)`);
1618
+ const doc = await transport.request({ method: "POST", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { op }, responseType: "json" });
1619
+ if (!doc || typeof doc.id !== "string" || !doc.id) throw new ApiError(`rest-1 ${op} on ${oid} did not answer with an asset`, 200, "BAD_RESPONSE", doc);
1620
+ const moment = doc.id.split(":")[2] ?? null;
1621
+ return { oid: momentless(doc.id), moment };
1622
+ }
1623
+ async function template(transport, type, options = {}) {
1624
+ const doc = await transport.request({ method: "GET", path: `/rest-1.v1/New/${encodeURIComponent(type)}`, query: { ctx: options.ctx ? momentless(options.ctx) : void 0 }, responseType: "json" });
1625
+ const { _oid: _ignored, ...values } = fromAssetDocument({ ...doc, id: doc.id || `${type}:0` });
1626
+ return values;
1627
+ }
1490
1628
  var history = (transport, target, params) => {
1491
1629
  const [type, id] = target.split(":");
1492
1630
  const path = id ? `/rest-1.v1/Hist/${encodeURIComponent(type)}/${encodeURIComponent(id)}` : `/rest-1.v1/Hist/${encodeURIComponent(type)}`;
@@ -1494,6 +1632,8 @@ var history = (transport, target, params) => {
1494
1632
  };
1495
1633
 
1496
1634
  // src/client.ts
1635
+ var DEFAULT_LIMIT = 1e3;
1636
+ var ME_SELECT = ["Name", "Username", "Nickname", "Email"];
1497
1637
  var AgilityApiClient = class _AgilityApiClient {
1498
1638
  transport;
1499
1639
  schema;
@@ -1525,6 +1665,48 @@ var AgilityApiClient = class _AgilityApiClient {
1525
1665
  const transport = { request: (options) => base.request({ ...options, signal: options.signal ?? signal }) };
1526
1666
  return new _AgilityApiClient(this.config, { transport, schema: this.schema });
1527
1667
  }
1668
+ async query(q) {
1669
+ await this.schema.ensureFor([q]);
1670
+ return query2(this.transport, q, { lookup: this.schema });
1671
+ }
1672
+ async queries(qs) {
1673
+ await this.schema.ensureFor(qs);
1674
+ return queries2(this.transport, [...qs], { lookup: this.schema });
1675
+ }
1676
+ async get(oid, select) {
1677
+ const rows = await this.query({ from: momentless(oid), ...select ? { select } : {}, page: { size: 1, start: 0 } });
1678
+ return rows[0] ?? null;
1679
+ }
1680
+ async queryPage(q) {
1681
+ await this.schema.ensureFor([q]);
1682
+ return queryPage(this.transport, q, { lookup: this.schema });
1683
+ }
1684
+ async *pages(q, options = {}) {
1685
+ if (!q.page) throw new UsageError(`query of ${q.from} without a page: every read must be bounded`);
1686
+ const limit = options.limit ?? DEFAULT_LIMIT;
1687
+ const size = q.page.size;
1688
+ let start = q.page.start, read2 = 0;
1689
+ while (read2 < limit) {
1690
+ const want = Math.min(size, limit - read2);
1691
+ const rows = await this.query({ ...q, page: { size: want, start } });
1692
+ if (rows.length) yield rows;
1693
+ read2 += rows.length;
1694
+ start += rows.length;
1695
+ if (rows.length < want) return;
1696
+ }
1697
+ }
1698
+ async all(q, options = {}) {
1699
+ const out = [];
1700
+ for await (const page2 of this.pages(q, options)) out.push(...page2);
1701
+ return out;
1702
+ }
1703
+ /** The member these credentials belong to (Member.IsSelf, live 2026-11-12): what a comment's Author is. */
1704
+ async me() {
1705
+ const [row] = await this.query({ from: "Member", select: ME_SELECT, filter: ["IsSelf='true'"], page: { size: 1, start: 0 } });
1706
+ if (!row) throw new ApiError("the server knows no member for these credentials", 0, "NOT_FOUND");
1707
+ return row;
1708
+ }
1709
+ // ---- the endpoints behind the reads -----------------------------------------------
1528
1710
  /** meta.v1: the instance's schema, raw. (The translated, cached view is `schema`.) */
1529
1711
  meta = {
1530
1712
  metamodel: () => getMetamodel(this.transport),
@@ -1533,12 +1715,88 @@ var AgilityApiClient = class _AgilityApiClient {
1533
1715
  };
1534
1716
  /**
1535
1717
  * loc-2.v1: labels for tags, in one request (meta's DisplayName values are
1536
- * tags). An unmatched tag comes back as itself; the server never says
1537
- * "missing", so neither does this.
1718
+ * tags, and so is a violation's `tag`). An unmatched tag comes back as
1719
+ * itself; the server never says "missing", so neither does this.
1538
1720
  */
1539
1721
  localize(tags, options) {
1540
1722
  return localize(this.transport, tags, options);
1541
1723
  }
1724
+ /**
1725
+ * rest-1.v1, for what the other endpoints cannot do: deleted assets
1726
+ * (`deleted: true`), one asset by OID without a page, history (one row
1727
+ * per moment), a new asset's template (`template`: what the type's
1728
+ * initializer fills in for a context — a Timebox's next name and dates,
1729
+ * a Story's carry-over from a Request), and an operation's result
1730
+ * (`operate`: api/asset performs a Copy but never says what it made;
1731
+ * rest-1 answers the new OID). Parameters are spelled as a query's
1732
+ * (`select`, `filter`).
1733
+ */
1734
+ data = {
1735
+ list: (type, params) => list2(this.transport, type, params),
1736
+ get: (oid, select) => get2(this.transport, oid, select),
1737
+ history: (target, params) => history(this.transport, target, params),
1738
+ template: (type, options) => template(this.transport, type, options),
1739
+ operate: (oid, op) => operate(this.transport, oid, op)
1740
+ };
1741
+ /** Copy an asset (the server's Copy operation) and answer the copy's OID: the one thing api/asset's `execute: "Copy"` does not say. */
1742
+ async copy(oid) {
1743
+ return (await operate(this.transport, oid, "Copy")).oid;
1744
+ }
1745
+ /**
1746
+ * What a form needs for an asset type in a scope: the attributes the
1747
+ * server requires to create one (meta), the ones the product shows there
1748
+ * (api/visibleFields, custom fields included once configured), and the
1749
+ * scope's scheme's important fields and whether they are enforced at
1750
+ * close. `visible` alone answers for several types and scopes at once.
1751
+ */
1752
+ fields = {
1753
+ describe: (type, scope) => describe2(this.transport, this.schema, type, scope),
1754
+ visible: (types, scopes) => visibleFields(this.transport, types, scopes),
1755
+ visibleByAsset: (oid) => visibleFieldsByAsset(this.transport, oid)
1756
+ };
1757
+ /** @deprecated since 1.0.0-alpha.3: use `client.query` / `client.queries` (the same endpoint, query.v1; only the name moved). Removed in 1.0.0. */
1758
+ queryV1 = {
1759
+ query: (q) => this.query(q),
1760
+ queries: (qs) => this.queries(qs)
1761
+ };
1762
+ // ---- writes -----------------------------------------------------------------------
1763
+ /** api/asset: batches, answered in the model's terms. */
1764
+ assets = {
1765
+ /**
1766
+ * Send a batch. The Outcome says what it did — created, modified and
1767
+ * operated OIDs, failures by index (each with its parsed violation),
1768
+ * query rows per query command — and never the envelope. api/asset
1769
+ * commits per command and answers 200 either way, so a failed command
1770
+ * is in `failures`, or thrown as a CommandFailureError with
1771
+ * `throwOnFailure` (the error carries the same Outcome).
1772
+ */
1773
+ send: async (commands, options) => {
1774
+ await this.schema.ensureFor(commands);
1775
+ return outcomeOf(await send(this.transport, commands, { ...options, lookup: this.schema }));
1776
+ },
1777
+ /** The server's projection of a batch: one command per asset it would touch, nothing applied. */
1778
+ preview: (commands) => preview(this.transport, commands),
1779
+ /**
1780
+ * Send as a compare-and-set: each asset is changed only if nobody has
1781
+ * changed it since the preview read it; the rest come back `skipped`.
1782
+ * Throws UnguardableError, before any request, for shapes it cannot
1783
+ * cover.
1784
+ */
1785
+ sendGuarded: async (commands, options) => {
1786
+ await this.schema.ensureFor(commands);
1787
+ return sendGuarded(this.transport, commands, { ...options, lookup: this.schema });
1788
+ },
1789
+ /** @deprecated since 1.0.0-alpha.3: use `client.query` (rows, through query.v1) or `client.queryPage` (rows and a total, through api/asset as this does). Removed in 1.0.0. */
1790
+ query: async (q) => {
1791
+ await this.schema.ensureFor([q]);
1792
+ return query(this.transport, q, { lookup: this.schema });
1793
+ },
1794
+ /** @deprecated since 1.0.0-alpha.3: use `client.queries`. Removed in 1.0.0. */
1795
+ queries: async (qs) => {
1796
+ await this.schema.ensureFor(qs);
1797
+ return queries(this.transport, qs, { lookup: this.schema });
1798
+ }
1799
+ };
1542
1800
  /**
1543
1801
  * Services: what a person types, turned into assets. Numbers resolve in
1544
1802
  * one api/asset request across every number root; an OID token is taken
@@ -1561,7 +1819,7 @@ var AgilityApiClient = class _AgilityApiClient {
1561
1819
  /**
1562
1820
  * The files attached to assets: metadata on Attachment assets, bytes
1563
1821
  * through attachment.img. `add` sends a small file inline (one request)
1564
- * and a large one in two steps; `url` is where a browser opens one.
1822
+ * and a large one in two steps.
1565
1823
  */
1566
1824
  attachments = {
1567
1825
  list: (asset, options) => list(this.transport, asset, options),
@@ -1571,7 +1829,8 @@ var AgilityApiClient = class _AgilityApiClient {
1571
1829
  add: (asset, data, options) => add(this.transport, asset, data, options),
1572
1830
  put: (id, data, contentType) => put(this.transport, id, data, contentType),
1573
1831
  remove: (id) => remove(this.transport, id),
1574
- url: (id) => attachmentUrl(this.config.baseUrl, id)
1832
+ /** @deprecated since 1.0.0-alpha.3: `client.urls.attachment`. Removed in 1.0.0. */
1833
+ url: (id) => this.urls.attachment(id)
1575
1834
  };
1576
1835
  /**
1577
1836
  * Images embedded in an asset's rich text (Description and other long
@@ -1581,55 +1840,27 @@ var AgilityApiClient = class _AgilityApiClient {
1581
1840
  */
1582
1841
  embeddedImages = {
1583
1842
  upload: (asset, data, options) => uploadEmbeddedImage(this.transport, asset, data, options),
1584
- url: (id) => embeddedImageUrl(this.config.baseUrl, id),
1843
+ /** @deprecated since 1.0.0-alpha.3: `client.urls.embeddedImage`. Removed in 1.0.0. */
1844
+ url: (id) => this.urls.embeddedImage(id),
1585
1845
  normalize: (html) => normalizeEmbeddedImageUrls(html, this.config.baseUrl)
1586
1846
  };
1587
- /** rest-1.v1: bounded reads, one asset, history. */
1588
- data = {
1589
- list: (type, params) => list2(this.transport, type, params),
1590
- get: (oid, sel) => get2(this.transport, oid, sel),
1591
- history: (target, params) => history(this.transport, target, params)
1592
- };
1593
- /**
1594
- * query.v1: reads only, the same query bodies as assets.query, rows
1595
- * translated the same way. The endpoint cannot write, so it is the safe
1596
- * place to explore a query; its requests may be retried like a GET.
1597
- */
1598
- queryV1 = {
1599
- query: async (q) => {
1600
- await this.schema.ensureFor([q]);
1601
- return query2(this.transport, q, { lookup: this.schema });
1602
- },
1603
- queries: async (qs) => {
1604
- await this.schema.ensureFor(qs);
1605
- return queries2(this.transport, qs, { lookup: this.schema });
1606
- }
1607
- };
1608
- /** api/asset: batches and queries, rows translated through the schema. */
1609
- assets = {
1610
- send: async (commands, options) => {
1611
- await this.schema.ensureFor(commands);
1612
- return send(this.transport, commands, { ...options, lookup: this.schema });
1613
- },
1614
- preview: (commands) => preview(this.transport, commands),
1847
+ /** Where a browser opens things on this instance: an asset's page, an attachment's bytes, an embedded image. */
1848
+ urls = {
1615
1849
  /**
1616
- * Send as a compare-and-set: each asset is changed only if nobody has
1617
- * changed it since the preview read it; the rest come back `skipped`.
1618
- * Throws UnguardableError, before any request, for shapes it cannot
1619
- * cover.
1850
+ * The asset's page, from an OID token or an asset Number (S-01001,
1851
+ * what a person typed and what a message shows): assetdetail.v1
1852
+ * resolves either and redirects to the type's own detail page; a signed-
1853
+ * out reader lands there after logging in. Anything else is a UsageError.
1620
1854
  */
1621
- sendGuarded: async (commands, options) => {
1622
- await this.schema.ensureFor(commands);
1623
- return sendGuarded(this.transport, commands, { ...options, lookup: this.schema });
1624
- },
1625
- query: async (q) => {
1626
- await this.schema.ensureFor([q]);
1627
- return query(this.transport, q, { lookup: this.schema });
1855
+ asset: (ref) => {
1856
+ const base = this.config.baseUrl.replace(/\/+$/, "");
1857
+ const text = ref.trim();
1858
+ if (isOidToken(text)) return `${base}/assetdetail.v1?oid=${momentless(text)}`;
1859
+ if (isAssetNumber(text)) return `${base}/assetdetail.v1?Number=${encodeURIComponent(text)}`;
1860
+ throw new UsageError(`"${ref}" is neither an OID token (Story:1234) nor an asset number (S-1234)`);
1628
1861
  },
1629
- queries: async (qs) => {
1630
- await this.schema.ensureFor(qs);
1631
- return queries(this.transport, qs, { lookup: this.schema });
1632
- }
1862
+ attachment: (id) => attachmentUrl(this.config.baseUrl, id),
1863
+ embeddedImage: (id) => embeddedImageUrl(this.config.baseUrl, id)
1633
1864
  };
1634
1865
  };
1635
1866
  // Annotate the CommonJS export names for ESM import in node:
@@ -1649,19 +1880,25 @@ var AgilityApiClient = class _AgilityApiClient {
1649
1880
  TypeCache,
1650
1881
  UNGUARDABLE_TYPES,
1651
1882
  UnguardableError,
1883
+ UsageError,
1884
+ WrappedError,
1652
1885
  assetApi,
1653
1886
  assetStateOf,
1654
1887
  attachment,
1655
1888
  createFetchTransport,
1656
1889
  describeBatch,
1890
+ fields,
1657
1891
  guard,
1892
+ importantName,
1658
1893
  isAssetNumber,
1659
1894
  isOidToken,
1895
+ kindOf,
1660
1896
  loc2,
1661
1897
  meta,
1662
1898
  momentless,
1663
1899
  namesOf,
1664
1900
  parseOid,
1901
+ parseViolation,
1665
1902
  queryV1,
1666
1903
  resolveNumbers,
1667
1904
  resolveRef,