@versionone/agility-sdk 1.0.0-alpha.2 → 1.0.0-alpha.3
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/README.md +17 -10
- package/dist/index.cjs +205 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4112 -3883
- package/dist/index.d.ts +4112 -3883
- package/dist/index.js +201 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,8 @@ npm install @versionone/agility-sdk@alpha
|
|
|
16
16
|
import { AgilityApiClient } from "@versionone/agility-sdk";
|
|
17
17
|
|
|
18
18
|
const client = new AgilityApiClient({ baseUrl: "https://www14.v1host.com/v1sdktesting", accessToken: process.env.V1_ACCESS_TOKEN });
|
|
19
|
-
const stories = await client.
|
|
19
|
+
const stories = await client.query({ from: "Story", select: ["Name", "Estimate"], page: { size: 10, start: 0 } });
|
|
20
|
+
// stories[0].Estimate is a number | null; stories[0].Nmae does not compile
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
## Working on the SDK
|
|
@@ -34,20 +35,26 @@ npm test
|
|
|
34
35
|
- `npm run typecheck` - run TypeScript type checks
|
|
35
36
|
- `npm run verify` - run typecheck + build + tests
|
|
36
37
|
- `npm run test` - run unit tests
|
|
37
|
-
- `npm run download:metamodel -- <baseAgilityUrl>` -
|
|
38
|
+
- `npm run download:metamodel -- <baseAgilityUrl>` - refresh `metamodel.json` from an instance on the current release (instance-specific definitions stripped; `--raw` keeps them), then `npm run generate:types`. The cleanest source is a dev instance on a blank database (Core's gulp task for that); a hosted instance on the release gives the same file. The generated model is from meta.v1 **26.3.1.13** (hosted, 2026-11-12)
|
|
38
39
|
|
|
39
40
|
## Public API
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
Changes between alphas are in `CHANGELOG.md`, each with its migration line.
|
|
43
|
+
|
|
44
|
+
- `AgilityApiClient` - the client. One query body everywhere; the client picks the endpoint:
|
|
45
|
+
- `query` / `queries` - rows, translated (numbers as numbers, booleans as booleans, a single relation as one `Ref`), through query.v1: read-only, so a request is retried like a GET. A page is required; a refused query throws, never an empty result. Written with a literal `from`, `select` completes from the type's attributes and the rows are typed by what was selected
|
|
46
|
+
- `get(oid, select)` - one asset or null; `queryPage(q)` - rows with the total and whether more follow; `pages(q, { limit })` / `all(q, { limit })` - page by page up to a limit (default 1000); `me()` - the member the credentials belong to
|
|
47
|
+
- `assets.send` - an api/asset batch, answered as an `Outcome` (`created`, `modified`, `operated`, `failures` with parsed violations, `queryResults`); `throwOnFailure` throws a `CommandFailureError` carrying the same Outcome. `assets.preview` - the projection. `assets.sendGuarded` - a batch as a compare-and-set on each asset's ChangeDate; changed assets come back `skipped`
|
|
48
|
+
- `batch.run` / `batch.reversible` - preview, snapshot, (confirm,) send guarded, and a receipt with a reverse batch; undo reactivates a closed asset before deleting it and reopens what a close cascaded to
|
|
43
49
|
- `resolve.numbers` / `resolve.ref` - asset numbers and OID tokens to assets, in one request
|
|
44
|
-
- `
|
|
45
|
-
- `
|
|
46
|
-
- `
|
|
47
|
-
- `
|
|
48
|
-
-
|
|
50
|
+
- `attachments.list` / `page` / `info` / `get` / `add` / `put` / `remove` - files attached to an asset; `add` sends a small file inline and a large one in two steps, and a failed upload throws `AttachmentUploadError` naming the empty attachment it left
|
|
51
|
+
- `embeddedImages.upload` / `normalize` - images in rich text; they cannot be deleted
|
|
52
|
+
- `urls.asset` / `attachment` / `embeddedImage` - where a browser opens each
|
|
53
|
+
- `data.list` / `get` / `history` - rest-1 reads, for deleted assets and history; `meta.*` - meta.v1 raw; `schema` - the translated, cached view; `localize` - loc-2 labels for tags (a violation's `tag` is one)
|
|
54
|
+
- Errors: everything thrown is an `ApiError` with a `kind` (`auth`, `forbidden`, `notFound`, `invalid`, `transient`, `aborted`, `commandFailure`, `protocol`, `usage`, `unknown`). A mistake caught before any request is a `UsageError`
|
|
55
|
+
- `TypeCache` - one instance's loaded type definitions, shared by many clients (`new AgilityApiClient(config, { types })`)
|
|
49
56
|
- `createFetchTransport` - configurable HTTP transport: `credentials` for same-origin cookie auth, `onUnauthorized` to re-authenticate on a 401 (resends idempotent requests once), and a per-request `signal`; `client.withSignal(signal)` ties a whole client to one
|
|
50
|
-
- `
|
|
57
|
+
- The wire modules (`assetApi`, `queryV1`, `rest1`, `meta`, `loc2`, `attachment`) are exported for passthroughs and transport authors; they speak the endpoints' own shapes
|
|
51
58
|
|
|
52
59
|
API reference: https://docs.digital.ai/agility-api/
|
|
53
60
|
|
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,8 @@ __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,
|
|
@@ -43,11 +45,13 @@ __export(index_exports, {
|
|
|
43
45
|
guard: () => guard,
|
|
44
46
|
isAssetNumber: () => isAssetNumber,
|
|
45
47
|
isOidToken: () => isOidToken,
|
|
48
|
+
kindOf: () => kindOf,
|
|
46
49
|
loc2: () => loc2_exports,
|
|
47
50
|
meta: () => meta_exports,
|
|
48
51
|
momentless: () => momentless,
|
|
49
52
|
namesOf: () => namesOf,
|
|
50
53
|
parseOid: () => parseOid,
|
|
54
|
+
parseViolation: () => parseViolation,
|
|
51
55
|
queryV1: () => queryV1_exports,
|
|
52
56
|
resolveNumbers: () => resolveNumbers,
|
|
53
57
|
resolveRef: () => resolveRef,
|
|
@@ -67,16 +71,61 @@ __export(index_exports, {
|
|
|
67
71
|
module.exports = __toCommonJS(index_exports);
|
|
68
72
|
|
|
69
73
|
// src/core/errors.ts
|
|
74
|
+
function kindOf(status, code) {
|
|
75
|
+
switch (code) {
|
|
76
|
+
case "ABORTED":
|
|
77
|
+
return "aborted";
|
|
78
|
+
case "TIMEOUT":
|
|
79
|
+
case "NETWORK_ERROR":
|
|
80
|
+
return "transient";
|
|
81
|
+
case "BAD_JSON":
|
|
82
|
+
case "BAD_RESPONSE":
|
|
83
|
+
return "protocol";
|
|
84
|
+
case "USAGE":
|
|
85
|
+
return "usage";
|
|
86
|
+
case "CommandFailure":
|
|
87
|
+
return "commandFailure";
|
|
88
|
+
case "QUERY_REJECTED":
|
|
89
|
+
case "TOO_LARGE":
|
|
90
|
+
return "invalid";
|
|
91
|
+
case "NOT_FOUND":
|
|
92
|
+
return "notFound";
|
|
93
|
+
}
|
|
94
|
+
if (status === 401) return "auth";
|
|
95
|
+
if (status === 403) return "forbidden";
|
|
96
|
+
if (status === 404) return "notFound";
|
|
97
|
+
if (status === 408 || status === 429 || status >= 500) return "transient";
|
|
98
|
+
if (status >= 400) return "invalid";
|
|
99
|
+
return "unknown";
|
|
100
|
+
}
|
|
101
|
+
var causeOf = (details) => details && typeof details === "object" && "cause" in details ? details.cause : void 0;
|
|
70
102
|
var ApiError = class extends Error {
|
|
103
|
+
/** The HTTP status; 0 when no answer came back (network, timeout, abort, usage). */
|
|
71
104
|
status;
|
|
72
105
|
code;
|
|
73
106
|
details;
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
kind;
|
|
108
|
+
constructor(message, status, code, details, kind = kindOf(status, code)) {
|
|
109
|
+
const cause = causeOf(details);
|
|
110
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
76
111
|
this.name = "ApiError";
|
|
77
112
|
this.status = status;
|
|
78
113
|
this.code = code;
|
|
79
114
|
this.details = details;
|
|
115
|
+
this.kind = kind;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var UsageError = class extends ApiError {
|
|
119
|
+
constructor(message, details) {
|
|
120
|
+
super(message, 0, "USAGE", details, "usage");
|
|
121
|
+
this.name = "UsageError";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var WrappedError = class extends ApiError {
|
|
125
|
+
constructor(message, code, cause, details) {
|
|
126
|
+
const inner = cause instanceof ApiError ? cause : void 0;
|
|
127
|
+
super(message, inner?.status ?? 0, code, { ...details, cause }, inner?.kind ?? "unknown");
|
|
128
|
+
this.name = "WrappedError";
|
|
80
129
|
}
|
|
81
130
|
};
|
|
82
131
|
|
|
@@ -216,7 +265,7 @@ function isOidToken(value) {
|
|
|
216
265
|
}
|
|
217
266
|
function parseOid(token) {
|
|
218
267
|
const m = OID_TOKEN.exec(token);
|
|
219
|
-
if (!m) throw new
|
|
268
|
+
if (!m) throw new UsageError(`"${token}" is not an OID token (Story:1234)`);
|
|
220
269
|
return { type: m[1], id: Number(m[2]), moment: m[3] === void 0 ? null : Number(m[3]) };
|
|
221
270
|
}
|
|
222
271
|
function momentless(token) {
|
|
@@ -240,6 +289,7 @@ __export(assetApi_exports, {
|
|
|
240
289
|
mergeOutcome: () => mergeOutcome,
|
|
241
290
|
momentless: () => momentless,
|
|
242
291
|
outcomeOf: () => outcomeOf,
|
|
292
|
+
parseViolation: () => parseViolation,
|
|
243
293
|
preview: () => preview,
|
|
244
294
|
queries: () => queries,
|
|
245
295
|
query: () => query,
|
|
@@ -405,11 +455,20 @@ function toNumber(value) {
|
|
|
405
455
|
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
406
456
|
return Number.isNaN(n) ? value : n;
|
|
407
457
|
}
|
|
458
|
+
function parseViolation(message) {
|
|
459
|
+
const parts = message.trim().split("'");
|
|
460
|
+
if (parts[0] !== "Violation" || !parts[1]) return null;
|
|
461
|
+
const rule = parts[1], rest = parts.slice(2).filter((p) => p !== "");
|
|
462
|
+
if (rest[0] === "AttributeDefinition" && rest[1] && rest[2]) return { rule, attribute: `${rest[2]}.${rest[1]}`, tag: rest.join("'") };
|
|
463
|
+
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;
|
|
464
|
+
return { rule, attribute: token, tag: rest.length ? rest.join("'") : null };
|
|
465
|
+
}
|
|
408
466
|
function failuresOf(response) {
|
|
409
467
|
return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => ({
|
|
410
468
|
index: typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
|
|
411
469
|
message: typeof error === "string" ? error : error?.message || "command failed",
|
|
412
|
-
echoed
|
|
470
|
+
echoed,
|
|
471
|
+
violation: parseViolation(typeof error === "string" ? error : error?.message || "")
|
|
413
472
|
}));
|
|
414
473
|
}
|
|
415
474
|
function outcomeOf(response) {
|
|
@@ -435,11 +494,14 @@ function mergeOutcome(into, done, offset) {
|
|
|
435
494
|
var CommandFailureError = class extends ApiError {
|
|
436
495
|
failures;
|
|
437
496
|
response;
|
|
497
|
+
/** What the batch did, in the model's terms: the commands that succeeded are committed. */
|
|
498
|
+
outcome;
|
|
438
499
|
constructor(response, failures = failuresOf(response)) {
|
|
439
500
|
super(failures.map((f) => f.message).join("; ") || "command failed", 200, "CommandFailure", failures);
|
|
440
501
|
this.name = "CommandFailureError";
|
|
441
502
|
this.failures = failures;
|
|
442
503
|
this.response = response;
|
|
504
|
+
this.outcome = outcomeOf(response);
|
|
443
505
|
}
|
|
444
506
|
};
|
|
445
507
|
var bodyOf = (c) => {
|
|
@@ -475,7 +537,7 @@ async function send(transport, commands, options = {}) {
|
|
|
475
537
|
const results = response.queryResult?.results ?? [];
|
|
476
538
|
let aligned = realign(queries3, results, failed);
|
|
477
539
|
if (!aligned) {
|
|
478
|
-
if (!failures.some((f) => f.index === null)) throw new
|
|
540
|
+
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
541
|
aligned = queries3.map(() => null);
|
|
480
542
|
}
|
|
481
543
|
const contextOf = (i) => {
|
|
@@ -497,7 +559,7 @@ async function query(transport, q, options = {}) {
|
|
|
497
559
|
return rows ?? [];
|
|
498
560
|
}
|
|
499
561
|
async function queryPage(transport, q, options = {}) {
|
|
500
|
-
if (!q.page) throw new
|
|
562
|
+
if (!q.page) throw new UsageError(`query of ${q.from} without a page: every api/asset read must be bounded`);
|
|
501
563
|
const response = await send(transport, [{ ...q, needTotal: true }], { ...options, throwOnFailure: true });
|
|
502
564
|
const rows = response.queryResult.results[0] ?? [];
|
|
503
565
|
const reported = response.queryResult.resultMetaData?.[0]?.total;
|
|
@@ -505,7 +567,7 @@ async function queryPage(transport, q, options = {}) {
|
|
|
505
567
|
return { rows, total, more: total === null ? rows.length === q.page.size : q.page.start + rows.length < total };
|
|
506
568
|
}
|
|
507
569
|
async function queries(transport, qs, options = {}) {
|
|
508
|
-
for (const q of qs) if (!q.page) throw new
|
|
570
|
+
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
571
|
const response = await send(transport, qs, { ...options, throwOnFailure: true });
|
|
510
572
|
return qs.map((_, i) => response.queryResult.results[i] ?? []);
|
|
511
573
|
}
|
|
@@ -543,7 +605,7 @@ function fromBase64(text) {
|
|
|
543
605
|
}
|
|
544
606
|
function numericId(spec) {
|
|
545
607
|
const m = /^(?:[A-Za-z]+:)?(\d+)(?::\d+)?$/.exec(String(spec).trim());
|
|
546
|
-
if (!m) throw new
|
|
608
|
+
if (!m) throw new UsageError(`"${spec}" is not an asset id (5678, or an OID token such as Attachment:5678)`);
|
|
547
609
|
return m[1];
|
|
548
610
|
}
|
|
549
611
|
var join = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path}`;
|
|
@@ -639,9 +701,9 @@ async function get(transport, id) {
|
|
|
639
701
|
const file = await getFile(transport, meta.id);
|
|
640
702
|
return { ...meta, bytes: file.bytes, contentType: file.contentType ?? meta.contentType, recordedContentType: meta.contentType };
|
|
641
703
|
}
|
|
642
|
-
var AttachmentUploadError = class extends
|
|
704
|
+
var AttachmentUploadError = class extends WrappedError {
|
|
643
705
|
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)}`,
|
|
706
|
+
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
707
|
this.attachment = attachment;
|
|
646
708
|
this.name = "AttachmentUploadError";
|
|
647
709
|
}
|
|
@@ -674,7 +736,7 @@ async function remove(transport, id) {
|
|
|
674
736
|
// src/services/guard.ts
|
|
675
737
|
var CHUNK = 100;
|
|
676
738
|
var UNGUARDABLE_TYPES = ["Idea", "IdeaChangeSet", "IdeaStatus", "IdeaComment", "IdeaCategory", "IdeaForum", "IdeaUser"];
|
|
677
|
-
var UnguardableError = class extends
|
|
739
|
+
var UnguardableError = class extends UsageError {
|
|
678
740
|
reasons;
|
|
679
741
|
constructor(reasons) {
|
|
680
742
|
super(`cannot guard this batch: ${reasons.map((r) => `command ${r.index}: ${r.reason}`).join("; ")}`);
|
|
@@ -740,7 +802,7 @@ function guard(projected, stamps) {
|
|
|
740
802
|
unstamped.push(x.oid);
|
|
741
803
|
continue;
|
|
742
804
|
}
|
|
743
|
-
if (!STAMP.test(stamp)) throw new
|
|
805
|
+
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
806
|
const scope = { from: typeOfOid(x.oid), filter: ["ID=$oid", `ChangeDate='${stamp}'`], with: { $oid: x.oid } };
|
|
745
807
|
commands.push(x.update ? { ...scope, update: x.update } : { ...scope, execute: x.execute });
|
|
746
808
|
}
|
|
@@ -757,16 +819,14 @@ function passesOf(projected) {
|
|
|
757
819
|
}
|
|
758
820
|
return passes;
|
|
759
821
|
}
|
|
760
|
-
var GuardedPassError = class extends
|
|
822
|
+
var GuardedPassError = class extends WrappedError {
|
|
761
823
|
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)}`,
|
|
763
|
-
this.cause = cause;
|
|
824
|
+
super(`guarded send failed in pass ${pass + 1}${pass ? " (earlier passes applied)" : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, "GUARDED_PASS", cause);
|
|
764
825
|
this.outcome = outcome;
|
|
765
826
|
this.sent = sent;
|
|
766
827
|
this.pass = pass;
|
|
767
828
|
this.name = "GuardedPassError";
|
|
768
829
|
}
|
|
769
|
-
cause;
|
|
770
830
|
outcome;
|
|
771
831
|
sent;
|
|
772
832
|
pass;
|
|
@@ -983,18 +1043,18 @@ function reverseOf(described, before, done, context = {}) {
|
|
|
983
1043
|
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
1044
|
return { commands, notes };
|
|
985
1045
|
}
|
|
986
|
-
var BatchRecordError = class extends
|
|
1046
|
+
var BatchRecordError = class extends WrappedError {
|
|
987
1047
|
receipt;
|
|
988
1048
|
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)}`,
|
|
1049
|
+
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
1050
|
this.name = "BatchRecordError";
|
|
991
1051
|
this.receipt = receipt;
|
|
992
1052
|
}
|
|
993
1053
|
};
|
|
994
|
-
var BatchSendError = class extends
|
|
1054
|
+
var BatchSendError = class extends WrappedError {
|
|
995
1055
|
receipt;
|
|
996
1056
|
constructor(cause, receipt) {
|
|
997
|
-
super(`the batch send failed; whether the server applied it is unknown: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1057
|
+
super(`the batch send failed; whether the server applied it is unknown: ${cause instanceof Error ? cause.message : String(cause)}`, "BATCH_SEND", cause);
|
|
998
1058
|
this.name = "BatchSendError";
|
|
999
1059
|
this.receipt = receipt;
|
|
1000
1060
|
}
|
|
@@ -1002,7 +1062,7 @@ var BatchSendError = class extends Error {
|
|
|
1002
1062
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1003
1063
|
async function runBatch(transport, schema, commands, options = {}) {
|
|
1004
1064
|
const useGuard = options.guard ?? true;
|
|
1005
|
-
if (!commands.length) throw new
|
|
1065
|
+
if (!commands.length) throw new UsageError("the batch is empty");
|
|
1006
1066
|
const refused = unguardable(commands);
|
|
1007
1067
|
const fatal = refused.filter((r) => r.reason.includes("rejected by the server"));
|
|
1008
1068
|
if (fatal.length || useGuard && refused.length) throw new UnguardableError(useGuard ? refused : fatal);
|
|
@@ -1173,7 +1233,7 @@ async function resolveNumbers(transport, numbers, options = {}) {
|
|
|
1173
1233
|
for (const n of numbers) if (!firsts.has(n.trim().toUpperCase())) firsts.set(n.trim().toUpperCase(), n.trim());
|
|
1174
1234
|
const asked = [...firsts.values()];
|
|
1175
1235
|
const invalid = asked.filter((n) => !isAssetNumber(n));
|
|
1176
|
-
if (invalid.length) throw new
|
|
1236
|
+
if (invalid.length) throw new UsageError(`not asset numbers (expected a prefix and digits, S-1234): ${invalid.join(", ")}`);
|
|
1177
1237
|
const byNumber = /* @__PURE__ */ new Map();
|
|
1178
1238
|
for (let i = 0; i < asked.length; i += CHUNK3) {
|
|
1179
1239
|
for (const r of await askRoots(transport, asked.slice(i, i + CHUNK3), options.types)) {
|
|
@@ -1194,7 +1254,7 @@ async function resolveRef(transport, spec, options = {}) {
|
|
|
1194
1254
|
return { number: null, oid, type: typeOfOid(oid), name: null, assetState: null };
|
|
1195
1255
|
}
|
|
1196
1256
|
if (isAssetNumber(text)) return (await resolveNumbers(transport, [text], options)).found[0] ?? null;
|
|
1197
|
-
throw new
|
|
1257
|
+
throw new UsageError(`"${spec}" is neither an asset number (S-1234) nor an OID token (Story:1234)`);
|
|
1198
1258
|
}
|
|
1199
1259
|
|
|
1200
1260
|
// src/wire/meta.ts
|
|
@@ -1256,7 +1316,7 @@ async function getAssetType(transport, token) {
|
|
|
1256
1316
|
}
|
|
1257
1317
|
async function getAttributeDefinition(transport, tokenref) {
|
|
1258
1318
|
const [type, attribute] = tokenref.split(".");
|
|
1259
|
-
if (!type || !attribute) throw new
|
|
1319
|
+
if (!type || !attribute) throw new UsageError(`"${tokenref}" is not an attribute token ("Story.Name")`);
|
|
1260
1320
|
return toAttributeDefinition(await transport.request({ method: "GET", path: `/meta.v1/${encodeURIComponent(type)}/${encodeURIComponent(attribute)}`, responseType: "json" }));
|
|
1261
1321
|
}
|
|
1262
1322
|
|
|
@@ -1383,7 +1443,7 @@ async function localize(transport, tags, options = {}) {
|
|
|
1383
1443
|
const encoded = encodeURIComponent(toLson(unique));
|
|
1384
1444
|
const headers = options.culture ? { "Accept-Language": options.culture } : void 0;
|
|
1385
1445
|
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
|
|
1446
|
+
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
1447
|
return result;
|
|
1388
1448
|
}
|
|
1389
1449
|
async function localizeOne(transport, tag, options = {}) {
|
|
@@ -1403,7 +1463,7 @@ function rejected(error) {
|
|
|
1403
1463
|
return messages.length ? new ApiError(messages.join("; "), error.status, "QUERY_REJECTED", error.details) : error;
|
|
1404
1464
|
}
|
|
1405
1465
|
async function queries2(transport, qs, options = {}) {
|
|
1406
|
-
for (const q of qs) if (!q.page) throw new
|
|
1466
|
+
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
1467
|
if (!qs.length) return [];
|
|
1408
1468
|
let response;
|
|
1409
1469
|
try {
|
|
@@ -1464,8 +1524,8 @@ function fromAssetDocument(doc, { history: history2 = false } = {}) {
|
|
|
1464
1524
|
}
|
|
1465
1525
|
function toQuery(params) {
|
|
1466
1526
|
return {
|
|
1467
|
-
sel: params.sel?.join(","),
|
|
1468
|
-
where: params.where?.length ? params.where.join(";") : void 0,
|
|
1527
|
+
sel: (params.select ?? params.sel)?.join(","),
|
|
1528
|
+
where: (params.filter ?? params.where)?.length ? (params.filter ?? params.where).join(";") : void 0,
|
|
1469
1529
|
with: params.with ? Object.entries(params.with).map(([k, v]) => `${k}=${[].concat(v).join(",")}`).join("|") : void 0,
|
|
1470
1530
|
find: params.find,
|
|
1471
1531
|
findin: params.findin?.join(","),
|
|
@@ -1477,14 +1537,14 @@ function toQuery(params) {
|
|
|
1477
1537
|
};
|
|
1478
1538
|
}
|
|
1479
1539
|
async function read(transport, path, params, history2) {
|
|
1480
|
-
if (!params.page) throw new
|
|
1540
|
+
if (!params.page) throw new UsageError(`rest-1 read of ${path} without a page: every read must be bounded`);
|
|
1481
1541
|
const doc = await transport.request({ method: "GET", path, query: toQuery(params), responseType: "json" });
|
|
1482
1542
|
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
1543
|
}
|
|
1484
1544
|
var list2 = (transport, type, params) => read(transport, `/rest-1.v1/Data/${encodeURIComponent(type)}`, params, false);
|
|
1485
|
-
async function get2(transport, oid,
|
|
1545
|
+
async function get2(transport, oid, select) {
|
|
1486
1546
|
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:
|
|
1547
|
+
const doc = await transport.request({ method: "GET", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { sel: select?.join(",") }, responseType: "json" });
|
|
1488
1548
|
return fromAssetDocument(doc);
|
|
1489
1549
|
}
|
|
1490
1550
|
var history = (transport, target, params) => {
|
|
@@ -1494,6 +1554,8 @@ var history = (transport, target, params) => {
|
|
|
1494
1554
|
};
|
|
1495
1555
|
|
|
1496
1556
|
// src/client.ts
|
|
1557
|
+
var DEFAULT_LIMIT = 1e3;
|
|
1558
|
+
var ME_SELECT = ["Name", "Username", "Nickname", "Email"];
|
|
1497
1559
|
var AgilityApiClient = class _AgilityApiClient {
|
|
1498
1560
|
transport;
|
|
1499
1561
|
schema;
|
|
@@ -1525,6 +1587,48 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1525
1587
|
const transport = { request: (options) => base.request({ ...options, signal: options.signal ?? signal }) };
|
|
1526
1588
|
return new _AgilityApiClient(this.config, { transport, schema: this.schema });
|
|
1527
1589
|
}
|
|
1590
|
+
async query(q) {
|
|
1591
|
+
await this.schema.ensureFor([q]);
|
|
1592
|
+
return query2(this.transport, q, { lookup: this.schema });
|
|
1593
|
+
}
|
|
1594
|
+
async queries(qs) {
|
|
1595
|
+
await this.schema.ensureFor(qs);
|
|
1596
|
+
return queries2(this.transport, [...qs], { lookup: this.schema });
|
|
1597
|
+
}
|
|
1598
|
+
async get(oid, select) {
|
|
1599
|
+
const rows = await this.query({ from: momentless(oid), ...select ? { select } : {}, page: { size: 1, start: 0 } });
|
|
1600
|
+
return rows[0] ?? null;
|
|
1601
|
+
}
|
|
1602
|
+
async queryPage(q) {
|
|
1603
|
+
await this.schema.ensureFor([q]);
|
|
1604
|
+
return queryPage(this.transport, q, { lookup: this.schema });
|
|
1605
|
+
}
|
|
1606
|
+
async *pages(q, options = {}) {
|
|
1607
|
+
if (!q.page) throw new UsageError(`query of ${q.from} without a page: every read must be bounded`);
|
|
1608
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
1609
|
+
const size = q.page.size;
|
|
1610
|
+
let start = q.page.start, read2 = 0;
|
|
1611
|
+
while (read2 < limit) {
|
|
1612
|
+
const want = Math.min(size, limit - read2);
|
|
1613
|
+
const rows = await this.query({ ...q, page: { size: want, start } });
|
|
1614
|
+
if (rows.length) yield rows;
|
|
1615
|
+
read2 += rows.length;
|
|
1616
|
+
start += rows.length;
|
|
1617
|
+
if (rows.length < want) return;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
async all(q, options = {}) {
|
|
1621
|
+
const out = [];
|
|
1622
|
+
for await (const page2 of this.pages(q, options)) out.push(...page2);
|
|
1623
|
+
return out;
|
|
1624
|
+
}
|
|
1625
|
+
/** The member these credentials belong to (Member.IsSelf, live 2026-11-12): what a comment's Author is. */
|
|
1626
|
+
async me() {
|
|
1627
|
+
const [row] = await this.query({ from: "Member", select: ME_SELECT, filter: ["IsSelf='true'"], page: { size: 1, start: 0 } });
|
|
1628
|
+
if (!row) throw new ApiError("the server knows no member for these credentials", 0, "NOT_FOUND");
|
|
1629
|
+
return row;
|
|
1630
|
+
}
|
|
1631
|
+
// ---- the endpoints behind the reads -----------------------------------------------
|
|
1528
1632
|
/** meta.v1: the instance's schema, raw. (The translated, cached view is `schema`.) */
|
|
1529
1633
|
meta = {
|
|
1530
1634
|
metamodel: () => getMetamodel(this.transport),
|
|
@@ -1533,12 +1637,65 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1533
1637
|
};
|
|
1534
1638
|
/**
|
|
1535
1639
|
* loc-2.v1: labels for tags, in one request (meta's DisplayName values are
|
|
1536
|
-
* tags). An unmatched tag comes back as
|
|
1537
|
-
* "missing", so neither does this.
|
|
1640
|
+
* tags, and so is a violation's `tag`). An unmatched tag comes back as
|
|
1641
|
+
* itself; the server never says "missing", so neither does this.
|
|
1538
1642
|
*/
|
|
1539
1643
|
localize(tags, options) {
|
|
1540
1644
|
return localize(this.transport, tags, options);
|
|
1541
1645
|
}
|
|
1646
|
+
/**
|
|
1647
|
+
* rest-1.v1, for what query.v1 cannot do: deleted assets (`deleted:
|
|
1648
|
+
* true`), one asset by OID without a page, and history (one row per
|
|
1649
|
+
* moment). Parameters are spelled as a query's (`select`, `filter`).
|
|
1650
|
+
*/
|
|
1651
|
+
data = {
|
|
1652
|
+
list: (type, params) => list2(this.transport, type, params),
|
|
1653
|
+
get: (oid, select) => get2(this.transport, oid, select),
|
|
1654
|
+
history: (target, params) => history(this.transport, target, params)
|
|
1655
|
+
};
|
|
1656
|
+
/** @deprecated since 1.0.0-alpha.3: `client.query` / `client.queries` read through query.v1. Removed in 1.0.0. */
|
|
1657
|
+
queryV1 = {
|
|
1658
|
+
query: (q) => this.query(q),
|
|
1659
|
+
queries: (qs) => this.queries(qs)
|
|
1660
|
+
};
|
|
1661
|
+
// ---- writes -----------------------------------------------------------------------
|
|
1662
|
+
/** api/asset: batches, answered in the model's terms. */
|
|
1663
|
+
assets = {
|
|
1664
|
+
/**
|
|
1665
|
+
* Send a batch. The Outcome says what it did — created, modified and
|
|
1666
|
+
* operated OIDs, failures by index (each with its parsed violation),
|
|
1667
|
+
* query rows per query command — and never the envelope. api/asset
|
|
1668
|
+
* commits per command and answers 200 either way, so a failed command
|
|
1669
|
+
* is in `failures`, or thrown as a CommandFailureError with
|
|
1670
|
+
* `throwOnFailure` (the error carries the same Outcome).
|
|
1671
|
+
*/
|
|
1672
|
+
send: async (commands, options) => {
|
|
1673
|
+
await this.schema.ensureFor(commands);
|
|
1674
|
+
return outcomeOf(await send(this.transport, commands, { ...options, lookup: this.schema }));
|
|
1675
|
+
},
|
|
1676
|
+
/** The server's projection of a batch: one command per asset it would touch, nothing applied. */
|
|
1677
|
+
preview: (commands) => preview(this.transport, commands),
|
|
1678
|
+
/**
|
|
1679
|
+
* Send as a compare-and-set: each asset is changed only if nobody has
|
|
1680
|
+
* changed it since the preview read it; the rest come back `skipped`.
|
|
1681
|
+
* Throws UnguardableError, before any request, for shapes it cannot
|
|
1682
|
+
* cover.
|
|
1683
|
+
*/
|
|
1684
|
+
sendGuarded: async (commands, options) => {
|
|
1685
|
+
await this.schema.ensureFor(commands);
|
|
1686
|
+
return sendGuarded(this.transport, commands, { ...options, lookup: this.schema });
|
|
1687
|
+
},
|
|
1688
|
+
/** @deprecated since 1.0.0-alpha.3: use `client.query` (or `client.queryPage` for a total). Removed in 1.0.0. */
|
|
1689
|
+
query: async (q) => {
|
|
1690
|
+
await this.schema.ensureFor([q]);
|
|
1691
|
+
return query(this.transport, q, { lookup: this.schema });
|
|
1692
|
+
},
|
|
1693
|
+
/** @deprecated since 1.0.0-alpha.3: use `client.queries`. Removed in 1.0.0. */
|
|
1694
|
+
queries: async (qs) => {
|
|
1695
|
+
await this.schema.ensureFor(qs);
|
|
1696
|
+
return queries(this.transport, qs, { lookup: this.schema });
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1542
1699
|
/**
|
|
1543
1700
|
* Services: what a person types, turned into assets. Numbers resolve in
|
|
1544
1701
|
* one api/asset request across every number root; an OID token is taken
|
|
@@ -1561,7 +1718,7 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1561
1718
|
/**
|
|
1562
1719
|
* The files attached to assets: metadata on Attachment assets, bytes
|
|
1563
1720
|
* through attachment.img. `add` sends a small file inline (one request)
|
|
1564
|
-
* and a large one in two steps
|
|
1721
|
+
* and a large one in two steps.
|
|
1565
1722
|
*/
|
|
1566
1723
|
attachments = {
|
|
1567
1724
|
list: (asset, options) => list(this.transport, asset, options),
|
|
@@ -1571,7 +1728,8 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1571
1728
|
add: (asset, data, options) => add(this.transport, asset, data, options),
|
|
1572
1729
|
put: (id, data, contentType) => put(this.transport, id, data, contentType),
|
|
1573
1730
|
remove: (id) => remove(this.transport, id),
|
|
1574
|
-
|
|
1731
|
+
/** @deprecated since 1.0.0-alpha.3: `client.urls.attachment`. Removed in 1.0.0. */
|
|
1732
|
+
url: (id) => this.urls.attachment(id)
|
|
1575
1733
|
};
|
|
1576
1734
|
/**
|
|
1577
1735
|
* Images embedded in an asset's rich text (Description and other long
|
|
@@ -1581,55 +1739,16 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1581
1739
|
*/
|
|
1582
1740
|
embeddedImages = {
|
|
1583
1741
|
upload: (asset, data, options) => uploadEmbeddedImage(this.transport, asset, data, options),
|
|
1584
|
-
|
|
1742
|
+
/** @deprecated since 1.0.0-alpha.3: `client.urls.embeddedImage`. Removed in 1.0.0. */
|
|
1743
|
+
url: (id) => this.urls.embeddedImage(id),
|
|
1585
1744
|
normalize: (html) => normalizeEmbeddedImageUrls(html, this.config.baseUrl)
|
|
1586
1745
|
};
|
|
1587
|
-
/**
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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),
|
|
1615
|
-
/**
|
|
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.
|
|
1620
|
-
*/
|
|
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 });
|
|
1628
|
-
},
|
|
1629
|
-
queries: async (qs) => {
|
|
1630
|
-
await this.schema.ensureFor(qs);
|
|
1631
|
-
return queries(this.transport, qs, { lookup: this.schema });
|
|
1632
|
-
}
|
|
1746
|
+
/** Where a browser opens things on this instance: an asset's page, an attachment's bytes, an embedded image. */
|
|
1747
|
+
urls = {
|
|
1748
|
+
/** The asset's page: assetdetail.v1 redirects to the type's own detail page (live 2026-11-12). */
|
|
1749
|
+
asset: (oid) => `${this.config.baseUrl.replace(/\/+$/, "")}/assetdetail.v1?oid=${momentless(oid)}`,
|
|
1750
|
+
attachment: (id) => attachmentUrl(this.config.baseUrl, id),
|
|
1751
|
+
embeddedImage: (id) => embeddedImageUrl(this.config.baseUrl, id)
|
|
1633
1752
|
};
|
|
1634
1753
|
};
|
|
1635
1754
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1649,6 +1768,8 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1649
1768
|
TypeCache,
|
|
1650
1769
|
UNGUARDABLE_TYPES,
|
|
1651
1770
|
UnguardableError,
|
|
1771
|
+
UsageError,
|
|
1772
|
+
WrappedError,
|
|
1652
1773
|
assetApi,
|
|
1653
1774
|
assetStateOf,
|
|
1654
1775
|
attachment,
|
|
@@ -1657,11 +1778,13 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1657
1778
|
guard,
|
|
1658
1779
|
isAssetNumber,
|
|
1659
1780
|
isOidToken,
|
|
1781
|
+
kindOf,
|
|
1660
1782
|
loc2,
|
|
1661
1783
|
meta,
|
|
1662
1784
|
momentless,
|
|
1663
1785
|
namesOf,
|
|
1664
1786
|
parseOid,
|
|
1787
|
+
parseViolation,
|
|
1665
1788
|
queryV1,
|
|
1666
1789
|
resolveNumbers,
|
|
1667
1790
|
resolveRef,
|