@versionone/agility-sdk 1.0.0-alpha.3 → 1.0.0-alpha.5
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/CHANGELOG.md +171 -0
- package/README.md +9 -3
- package/dist/index.cjs +225 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -107
- package/dist/index.d.ts +301 -107
- package/dist/index.js +223 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0-alpha.5 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Conditional schema reads** (agility-rooms D1/D3). meta.v1 sends one
|
|
8
|
+
strong ETag for the whole schema (`"26.3.1.13.<stamp>"`, the same on
|
|
9
|
+
the document, a type and an attribute) and honours `If-None-Match` with
|
|
10
|
+
a 304. `client.meta.read({ ifNoneMatch })` and `readType(token, {
|
|
11
|
+
ifNoneMatch })` answer `{ value, etag }`, or null without a download when
|
|
12
|
+
the tag still matches; `client.meta.etag()` reads the tag alone from
|
|
13
|
+
the smallest resource. `TypeCache.etag` remembers the tag the loaded
|
|
14
|
+
types came with, and `client.schema.revalidate()` asks the server in
|
|
15
|
+
one small read and drops the loaded types only when the tag moved —
|
|
16
|
+
what rooms' metamodel store does by hand. Transport: a request with
|
|
17
|
+
`acceptNotModified` resolves to undefined on a 304 after `onResponse`
|
|
18
|
+
sees it.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Idempotent requests are resent once by default** (`retries` now
|
|
23
|
+
defaults to 1, was 0): a GET, a query.v1 read or a guarded batch that
|
|
24
|
+
says so, after a network failure or a 5xx, 150 ms later. A plain write
|
|
25
|
+
is never resent, as before. Pass `retries: 0` for the old behaviour.
|
|
26
|
+
(gh-v1: the doc promised a retry the default never made.) A 429 counts
|
|
27
|
+
as transient like a 5xx — the instance throttles per token — and a
|
|
28
|
+
Retry-After is honoured when sent, up to 30 s.
|
|
29
|
+
- **A rejected query's `details` are the exceptions alone**
|
|
30
|
+
(`{ type, message }[]`, exported as `QueryRejection`), not query.v1's
|
|
31
|
+
whole error document; `message` is unchanged. (gh-v1: a formatter that
|
|
32
|
+
printed details printed a JSON document.)
|
|
33
|
+
- **`CommandFailureDetail.echoed` is not enumerable**: still readable on
|
|
34
|
+
every failure for a corrected resend, but a serialised failure (a
|
|
35
|
+
receipt, a log line, a bot's result) carries index, message and
|
|
36
|
+
violation only. (slack bot, gh-v1: re-mapping failures to keep results
|
|
37
|
+
small.)
|
|
38
|
+
|
|
39
|
+
## 1.0.0-alpha.4 — what the three adoptions asked for
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Two narrow rest-1 paths** (agility-rooms): `client.data.operate(oid,
|
|
44
|
+
op)` runs an operation for its result — api/asset performs a Copy but
|
|
45
|
+
reports only the source; rest-1 answers the copy's OID — and
|
|
46
|
+
`client.copy(oid)` is that for Copy. `client.data.template(type, {
|
|
47
|
+
ctx })` reads `New/<Type>?ctx=`: what the type's initializer fills in
|
|
48
|
+
for a context (a Timebox's next name and dates, schedule, state and
|
|
49
|
+
owner; a Story's carry-over from a Request), translated like a row,
|
|
50
|
+
without an `_oid`. Neither sends attribute values: rest-1 still carries
|
|
51
|
+
no update path.
|
|
52
|
+
- **`client.fields`**: `describe(type, scope)` joins what a form needs —
|
|
53
|
+
the attributes the server requires on a create (meta), the ones the
|
|
54
|
+
product shows for the type in that scope (`api/visibleFields`, custom
|
|
55
|
+
fields included once configured), and the scope's scheme's important
|
|
56
|
+
fields with whether they are enforced at close; `visible(types,
|
|
57
|
+
scopes)` and `visibleByAsset(oid)` are the endpoints alone. New wire
|
|
58
|
+
module `fields`.
|
|
59
|
+
- **`client.urls.asset` takes an asset Number** as well as an OID
|
|
60
|
+
(`assetdetail.v1?Number=S-01001`): the readable form a CLI prints and a
|
|
61
|
+
chat message shows (gh-v1, slack bot).
|
|
62
|
+
- **`Outcome.requestId`**: the server's id for a batch, worth logging
|
|
63
|
+
(slack bot). Null for an outcome merged from several sends.
|
|
64
|
+
- `outcomeOf` tolerating a partial envelope is now a stated contract
|
|
65
|
+
with a test: consumers' fakes may send only the fields they need.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- `parseViolation`: `tag` is null for the token spelling
|
|
70
|
+
(`Violation'Invalid'Epic.Scope`); it was the token, which is not a loc-2
|
|
71
|
+
tag (gh-v1).
|
|
72
|
+
- `CHANGELOG.md` ships in the npm package.
|
|
73
|
+
- Doc notes: `kind: "usage"` means nothing was sent (status 0), so an
|
|
74
|
+
error formatter should not blame the server; `client.query`'s resend
|
|
75
|
+
after a failure needs `retries` above its default of 0; the deprecation
|
|
76
|
+
notes on `queryV1` and `assets.query` say where each endpoint is.
|
|
77
|
+
|
|
78
|
+
### Migration note (from alpha.3's read path)
|
|
79
|
+
|
|
80
|
+
- A rejected query through `client.query` is an `ApiError` with kind
|
|
81
|
+
`invalid` and code `QUERY_REJECTED`; `message` holds the server's
|
|
82
|
+
sentences and `details` its whole error document. A formatter that
|
|
83
|
+
printed `details` for api/asset's `CommandFailureError` now prints a JSON
|
|
84
|
+
document; print `message`.
|
|
85
|
+
|
|
86
|
+
Alpha releases may break between versions. Every breaking change is listed
|
|
87
|
+
here with the one-line migration a consumer applies. A renamed export keeps
|
|
88
|
+
its old name as a `@deprecated` alias for one alpha and is removed in the
|
|
89
|
+
next; `1.0.0` follows agility-rooms' adoption, and semver after that.
|
|
90
|
+
|
|
91
|
+
## 1.0.0-alpha.3 — the breaking batch (plan 005, step 2)
|
|
92
|
+
|
|
93
|
+
### Breaking
|
|
94
|
+
|
|
95
|
+
- **`client.assets.send` answers an `Outcome`, not the api/asset envelope.**
|
|
96
|
+
`created`, `modified`, `operated` (momentless OIDs), `failures` (by index,
|
|
97
|
+
each with its parsed `violation`), `queryResults` (one slot per query
|
|
98
|
+
command, `null` where it failed), `durationSeconds`. Migration:
|
|
99
|
+
`res.assetsCreated.oidTokens` → `res.created`;
|
|
100
|
+
`res.commandFailures.count` → `res.failures.length`;
|
|
101
|
+
`res.queryResult.results[i]` → `res.queryResults[i]`. The wire module
|
|
102
|
+
`assetApi.send` still returns the envelope for a passthrough.
|
|
103
|
+
`CommandFailureError` gains `outcome` beside `response`.
|
|
104
|
+
- **Every SDK error is an `ApiError` with a `kind`** (`auth`, `forbidden`,
|
|
105
|
+
`notFound`, `invalid`, `transient`, `aborted`, `commandFailure`,
|
|
106
|
+
`protocol`, `usage`, `unknown`). Mistakes caught before any request
|
|
107
|
+
(an unpaged read, an empty batch, a bad OID token or asset number) throw
|
|
108
|
+
`UsageError`, not `Error`/`TypeError`. `UnguardableError` is a
|
|
109
|
+
`UsageError`; `GuardedPassError`, `BatchSendError`, `BatchRecordError`
|
|
110
|
+
and `AttachmentUploadError` are `WrappedError`s carrying the cause's
|
|
111
|
+
kind. Migration: a `catch` on `TypeError` becomes `instanceof
|
|
112
|
+
UsageError`; a branch on `status === 401` can become `kind === "auth"`.
|
|
113
|
+
- **rest-1 reads take a query's spelling**: `client.data.list(type, {
|
|
114
|
+
select, filter, … })`. `sel` and `where` still work this alpha
|
|
115
|
+
(deprecated) and go in the next.
|
|
116
|
+
|
|
117
|
+
### Deprecated (removed in the next alpha)
|
|
118
|
+
|
|
119
|
+
- `client.queryV1.query` / `.queries` → `client.query` / `client.queries`.
|
|
120
|
+
- `client.assets.query` / `.queries` → `client.query` / `client.queries`
|
|
121
|
+
(or `client.queryPage` for a total).
|
|
122
|
+
- `client.attachments.url`, `client.embeddedImages.url` →
|
|
123
|
+
`client.urls.attachment`, `client.urls.embeddedImage`.
|
|
124
|
+
- rest-1 `sel` / `where` → `select` / `filter`.
|
|
125
|
+
|
|
126
|
+
### Added
|
|
127
|
+
|
|
128
|
+
- The generated model is refreshed from meta.v1 26.3.1.13 (it was a
|
|
129
|
+
0.0.0.0 dev build): `Workflow`, `TeamWorkflow` and `WorkflowTransition`
|
|
130
|
+
arrive, with `Status.AllowedStatusTransitions`,
|
|
131
|
+
`Status.WorkflowStatusTransitions`, `Role.AllowedStatusTransitions` and
|
|
132
|
+
`Team.TeamWorkflows`. Nothing was removed or changed.
|
|
133
|
+
`scripts/download-metamodel.mjs` now strips an instance's own
|
|
134
|
+
definitions and reports them, so a refresh is reviewable.
|
|
135
|
+
- Reads on the client: `client.query`, `queries` (query.v1: read-only,
|
|
136
|
+
retried like a GET), `client.get(oid, select)`, `client.queryPage(q)`
|
|
137
|
+
(total and `more`, via api/asset), `client.pages(q, { limit })` and
|
|
138
|
+
`client.all(q, { limit })` (page by page up to a limit, default 1000),
|
|
139
|
+
`client.me()` (the member the credentials belong to).
|
|
140
|
+
- Typed queries: a literal `from` completes `select` from the type's
|
|
141
|
+
attributes, and rows are typed by what was selected (`row.Name` a
|
|
142
|
+
string, `row.Nmae` a compile error, `Status.Name` unknown, an aggregate
|
|
143
|
+
a number). An OID as `from` types by its type. `from` still takes an
|
|
144
|
+
alias or an unknown type; variables and `query<Story>` work as before.
|
|
145
|
+
- Custom list types (`Custom_<Name>`, the one custom asset type Agility
|
|
146
|
+
allows, a subtype of List) type as `CustomList`: List's attributes and
|
|
147
|
+
the type's own token as `AssetType`, as a `from` or as an OID. Custom
|
|
148
|
+
attributes (`Custom_…`) and the reciprocal side of a custom relation
|
|
149
|
+
(`ReciprocalOf_…`, which the server adds on the related type) are
|
|
150
|
+
accepted on every type as attribute values.
|
|
151
|
+
- `client.urls.asset(oid)`: the asset's page (`assetdetail.v1?oid=`).
|
|
152
|
+
- `parseViolation(message)` and `CommandFailureDetail.violation`: the
|
|
153
|
+
rule, attribute token and loc-2 tag of a validation failure.
|
|
154
|
+
- `kindOf(status, code)`, `ErrorKind`, `UsageError`, `WrappedError`.
|
|
155
|
+
|
|
156
|
+
## 1.0.0-alpha.2
|
|
157
|
+
|
|
158
|
+
- Releases are staged with `npm stage publish` and approved by a
|
|
159
|
+
maintainer with 2FA.
|
|
160
|
+
|
|
161
|
+
## 1.0.0-alpha.1
|
|
162
|
+
|
|
163
|
+
- Batch reverse uses the type's operations and the close cascade; the
|
|
164
|
+
guard sends a second command on one asset as a second pass; Moment is
|
|
165
|
+
never a guard stamp.
|
|
166
|
+
|
|
167
|
+
## 1.0.0-alpha.0
|
|
168
|
+
|
|
169
|
+
- First publish: `AgilityApiClient`, wire modules for api/asset,
|
|
170
|
+
query.v1, rest-1 reads, meta.v1, loc-2.v1 and attachments; services for
|
|
171
|
+
schema, resolve, guard, batch, attachments and embedded images.
|
package/README.md
CHANGED
|
@@ -49,8 +49,10 @@ Changes between alphas are in `CHANGELOG.md`, each with its migration line.
|
|
|
49
49
|
- `resolve.numbers` / `resolve.ref` - asset numbers and OID tokens to assets, in one request
|
|
50
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
51
|
- `embeddedImages.upload` / `normalize` - images in rich text; they cannot be deleted
|
|
52
|
-
- `
|
|
53
|
-
- `
|
|
52
|
+
- `copy(oid)` - the Copy operation, answering the copy's OID (api/asset performs it but never says what it made); `data.operate(oid, op)` is the general form
|
|
53
|
+
- `fields.describe(type, scope)` - what a form needs: required attributes (meta), the ones the product shows there (`api/visibleFields`, custom fields included), the scheme's important fields and whether they are enforced at close
|
|
54
|
+
- `urls.asset(oidOrNumber)` / `attachment` / `embeddedImage` - where a browser opens each
|
|
55
|
+
- `data.list` / `get` / `history` / `template` - rest-1 reads: deleted assets, history, and a new asset's template for a context (`New/<Type>?ctx=`); `meta.*` - meta.v1 raw, with conditional reads (`read` / `readType` with `ifNoneMatch`, `etag()`: meta.v1 sends one ETag for the whole schema); `schema` - the translated, cached view, `schema.revalidate()` drops it only when that tag moves; `localize` - loc-2 labels for tags (a violation's `tag` is one)
|
|
54
56
|
- 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
57
|
- `TypeCache` - one instance's loaded type definitions, shared by many clients (`new AgilityApiClient(config, { types })`)
|
|
56
58
|
- `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
|
|
@@ -75,7 +77,11 @@ No registry package yet. Pin a commit: `npm install github:versionone/VersionOne
|
|
|
75
77
|
Versions are published to npmjs by `.github/workflows/publish.yml` when a
|
|
76
78
|
tag `v<version>` is pushed; the tag must equal `package.json`'s version. A
|
|
77
79
|
prerelease (`1.0.0-alpha.0`, `-beta.N`, `-rc.N`) goes out on its own
|
|
78
|
-
dist-tag (`alpha`, `beta`, `rc`)
|
|
80
|
+
dist-tag (`alpha`, `beta`, `rc`). Until `1.0.0`, a maintainer moves
|
|
81
|
+
`latest` to the newest alpha by hand (`npm dist-tag add
|
|
82
|
+
@versionone/agility-sdk@<version> latest`), so a bare `npm install` gets
|
|
83
|
+
the current API rather than the first publish; from `1.0.0` on, `latest`
|
|
84
|
+
is never a prerelease.
|
|
79
85
|
|
|
80
86
|
```bash
|
|
81
87
|
npm version 1.0.0-alpha.2 --no-git-tag-version # bump, commit, merge
|
package/dist/index.cjs
CHANGED
|
@@ -42,7 +42,9 @@ __export(index_exports, {
|
|
|
42
42
|
attachment: () => attachment_exports,
|
|
43
43
|
createFetchTransport: () => createFetchTransport,
|
|
44
44
|
describeBatch: () => describe,
|
|
45
|
+
fields: () => fields_exports,
|
|
45
46
|
guard: () => guard,
|
|
47
|
+
importantName: () => importantName,
|
|
46
48
|
isAssetNumber: () => isAssetNumber,
|
|
47
49
|
isOidToken: () => isOidToken,
|
|
48
50
|
kindOf: () => kindOf,
|
|
@@ -183,10 +185,22 @@ function messageOf(data, status) {
|
|
|
183
185
|
return `HTTP ${status}`;
|
|
184
186
|
}
|
|
185
187
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
+
var MAX_RETRY_AFTER_MS = 3e4;
|
|
189
|
+
function retryDelay(response, attempt) {
|
|
190
|
+
const header = response.headers.get("retry-after");
|
|
191
|
+
if (header) {
|
|
192
|
+
const seconds = Number(header);
|
|
193
|
+
const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(header) - Date.now();
|
|
194
|
+
if (Number.isFinite(ms) && ms > MAX_RETRY_AFTER_MS) return null;
|
|
195
|
+
if (Number.isFinite(ms) && ms > 0) return ms;
|
|
196
|
+
}
|
|
197
|
+
return 150 * (attempt + 1);
|
|
198
|
+
}
|
|
199
|
+
var retriable = (status) => status === 429 || status >= 500;
|
|
186
200
|
function createFetchTransport(config) {
|
|
187
201
|
const fetchImpl = config.fetchImpl ?? fetch;
|
|
188
202
|
const timeoutMs = config.timeoutMs ?? 6e4;
|
|
189
|
-
const retries = Math.max(0, config.retries ??
|
|
203
|
+
const retries = Math.max(0, config.retries ?? 1);
|
|
190
204
|
const authorization = config.authorization ?? (config.accessToken ? `Bearer ${config.accessToken}` : void 0);
|
|
191
205
|
const unauthorizedStatuses = config.unauthorizedStatuses ?? [401];
|
|
192
206
|
return {
|
|
@@ -229,6 +243,10 @@ function createFetchTransport(config) {
|
|
|
229
243
|
options.onResponse?.({ status: response.status, headers: response.headers });
|
|
230
244
|
return await readBody(response, responseType);
|
|
231
245
|
}
|
|
246
|
+
if (response.status === 304 && options.acceptNotModified) {
|
|
247
|
+
options.onResponse?.({ status: 304, headers: response.headers });
|
|
248
|
+
return void 0;
|
|
249
|
+
}
|
|
232
250
|
const data = await readBody(response, responseType === "bytes" ? "text" : responseType === "json" ? "auto" : responseType);
|
|
233
251
|
lastError = new ApiError(messageOf(data, response.status), response.status, void 0, data);
|
|
234
252
|
} catch (error) {
|
|
@@ -247,8 +265,10 @@ function createFetchTransport(config) {
|
|
|
247
265
|
}
|
|
248
266
|
break;
|
|
249
267
|
}
|
|
250
|
-
if (response.status
|
|
251
|
-
|
|
268
|
+
if (retriable(response.status) && attempt + 1 < attempts) {
|
|
269
|
+
const wait = retryDelay(response, attempt);
|
|
270
|
+
if (wait === null) break;
|
|
271
|
+
await delay(wait);
|
|
252
272
|
continue;
|
|
253
273
|
}
|
|
254
274
|
break;
|
|
@@ -455,24 +475,29 @@ function toNumber(value) {
|
|
|
455
475
|
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
456
476
|
return Number.isNaN(n) ? value : n;
|
|
457
477
|
}
|
|
478
|
+
function failureDetail(index, message, echoed) {
|
|
479
|
+
const detail = { index, message, violation: parseViolation(message) };
|
|
480
|
+
Object.defineProperty(detail, "echoed", { value: echoed, enumerable: false, writable: false });
|
|
481
|
+
return detail;
|
|
482
|
+
}
|
|
458
483
|
function parseViolation(message) {
|
|
459
484
|
const parts = message.trim().split("'");
|
|
460
485
|
if (parts[0] !== "Violation" || !parts[1]) return null;
|
|
461
486
|
const rule = parts[1], rest = parts.slice(2).filter((p) => p !== "");
|
|
462
487
|
if (rest[0] === "AttributeDefinition" && rest[1] && rest[2]) return { rule, attribute: `${rest[2]}.${rest[1]}`, tag: rest.join("'") };
|
|
463
488
|
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 };
|
|
489
|
+
return { rule, attribute: token, tag: !token && rest.length > 1 ? rest.join("'") : null };
|
|
465
490
|
}
|
|
466
491
|
function failuresOf(response) {
|
|
467
|
-
return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
echoed
|
|
471
|
-
|
|
472
|
-
}));
|
|
492
|
+
return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => failureDetail(
|
|
493
|
+
typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
|
|
494
|
+
typeof error === "string" ? error : error?.message || "command failed",
|
|
495
|
+
echoed
|
|
496
|
+
));
|
|
473
497
|
}
|
|
474
498
|
function outcomeOf(response) {
|
|
475
499
|
return {
|
|
500
|
+
requestId: typeof response.requestId === "string" && response.requestId ? response.requestId : null,
|
|
476
501
|
created: (response.assetsCreated?.oidTokens ?? []).map(momentless),
|
|
477
502
|
modified: (response.assetsModified?.oidTokens ?? []).map(momentless),
|
|
478
503
|
operated: (response.assetsOperatedOn?.oidTokens ?? []).map(momentless),
|
|
@@ -481,13 +506,14 @@ function outcomeOf(response) {
|
|
|
481
506
|
durationSeconds: typeof response.durationSeconds === "number" ? response.durationSeconds : null
|
|
482
507
|
};
|
|
483
508
|
}
|
|
484
|
-
var emptyOutcome = () => ({ created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
|
|
509
|
+
var emptyOutcome = () => ({ requestId: null, created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
|
|
485
510
|
function mergeOutcome(into, done, offset) {
|
|
486
511
|
into.created.push(...done.created);
|
|
487
512
|
into.modified.push(...done.modified);
|
|
488
513
|
into.operated.push(...done.operated);
|
|
489
|
-
into.failures.push(...done.failures.map((f) => (
|
|
514
|
+
into.failures.push(...done.failures.map((f) => failureDetail(f.index === null ? null : f.index + offset, f.message, f.echoed)));
|
|
490
515
|
into.queryResults.push(...done.queryResults);
|
|
516
|
+
into.requestId = null;
|
|
491
517
|
if (done.durationSeconds !== null) into.durationSeconds = (into.durationSeconds ?? 0) + done.durationSeconds;
|
|
492
518
|
return into;
|
|
493
519
|
}
|
|
@@ -1168,6 +1194,65 @@ async function reversible(transport, receipt, options = {}) {
|
|
|
1168
1194
|
return { commands, drifted, reactivated };
|
|
1169
1195
|
}
|
|
1170
1196
|
|
|
1197
|
+
// src/wire/fields.ts
|
|
1198
|
+
var fields_exports = {};
|
|
1199
|
+
__export(fields_exports, {
|
|
1200
|
+
visibleFields: () => visibleFields,
|
|
1201
|
+
visibleFieldsByAsset: () => visibleFieldsByAsset
|
|
1202
|
+
});
|
|
1203
|
+
async function visibleFields(transport, types, scopes) {
|
|
1204
|
+
if (!types.length) return {};
|
|
1205
|
+
const params = [...scopes.map((s) => `scopeOid=${encodeURIComponent(momentless(s))}`), ...types.map((t) => `assetTypeToken=${encodeURIComponent(t)}`)].join("&");
|
|
1206
|
+
const answer = await transport.request({ method: "GET", path: `/api/visibleFields?${params}`, responseType: "json" });
|
|
1207
|
+
if (!Array.isArray(answer)) throw new ApiError("api/visibleFields did not answer a list", 200, "BAD_RESPONSE", answer);
|
|
1208
|
+
const out = {};
|
|
1209
|
+
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") : [];
|
|
1210
|
+
return out;
|
|
1211
|
+
}
|
|
1212
|
+
async function visibleFieldsByAsset(transport, oid) {
|
|
1213
|
+
const answer = await transport.request({ method: "GET", path: "/api/visibleFieldsByAsset", query: { oidToken: momentless(oid) }, responseType: "json" });
|
|
1214
|
+
if (!Array.isArray(answer)) throw new ApiError("api/visibleFieldsByAsset did not answer a list", 200, "BAD_RESPONSE", answer);
|
|
1215
|
+
return answer.filter((f) => typeof f === "string");
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// src/services/fields.ts
|
|
1219
|
+
async function lineage(schema, type) {
|
|
1220
|
+
const out = [];
|
|
1221
|
+
for (let t = type; t && !out.includes(t); ) {
|
|
1222
|
+
out.push(t);
|
|
1223
|
+
const def = await schema.load(t);
|
|
1224
|
+
t = def?.Base;
|
|
1225
|
+
}
|
|
1226
|
+
return out;
|
|
1227
|
+
}
|
|
1228
|
+
function importantName(token, types) {
|
|
1229
|
+
const dot = token.indexOf(".");
|
|
1230
|
+
if (dot < 0) return token;
|
|
1231
|
+
return types.includes(token.slice(0, dot)) ? token.slice(dot + 1) : null;
|
|
1232
|
+
}
|
|
1233
|
+
async function describe2(transport, schema, type, scope) {
|
|
1234
|
+
const scopeOid = momentless(scope);
|
|
1235
|
+
const types = await lineage(schema, type);
|
|
1236
|
+
const definition = await schema.load(type);
|
|
1237
|
+
const required = definition ? Object.values(definition.Attributes).filter((a) => a.IsRequired && !a.IsReadOnly).map((a) => a.Name).sort() : [];
|
|
1238
|
+
const [visible, [row]] = await Promise.all([
|
|
1239
|
+
visibleFields(transport, [type], [scopeOid]),
|
|
1240
|
+
query(transport, { from: "Scope", select: [{ from: "Scheme", select: ["ImportantFields", "IsRequiredAtClose"] }], filter: ["ID=$scope"], with: { $scope: scopeOid }, page: { size: 1, start: 0 } }, { lookup: schema })
|
|
1241
|
+
]);
|
|
1242
|
+
const scheme = row?.Scheme ?? null;
|
|
1243
|
+
const tokens = Array.isArray(scheme?.ImportantFields) ? scheme.ImportantFields.filter((t) => typeof t === "string") : [];
|
|
1244
|
+
const important = [...new Set(tokens.map((t) => importantName(t, types)).filter((n) => !!n))].sort();
|
|
1245
|
+
return {
|
|
1246
|
+
type,
|
|
1247
|
+
scope: scopeOid,
|
|
1248
|
+
scheme: scheme ? momentless(scheme._oid) : null,
|
|
1249
|
+
required,
|
|
1250
|
+
visible: visible[type] ?? [],
|
|
1251
|
+
important,
|
|
1252
|
+
requiredAtClose: scheme?.IsRequiredAtClose === true
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1171
1256
|
// src/types/asset.ts
|
|
1172
1257
|
var ASSET_STATE_CODES = { Future: 0, Active: 64, Closed: 128, Dead: 192, Template: 200, OldBrokenDownStory: 208, Deprecated: 254, Deleted: 255 };
|
|
1173
1258
|
var STATE_OF_CODE = Object.fromEntries(Object.entries(ASSET_STATE_CODES).map(([name, code]) => [code, name]));
|
|
@@ -1260,10 +1345,13 @@ async function resolveRef(transport, spec, options = {}) {
|
|
|
1260
1345
|
// src/wire/meta.ts
|
|
1261
1346
|
var meta_exports = {};
|
|
1262
1347
|
__export(meta_exports, {
|
|
1348
|
+
etag: () => etag,
|
|
1263
1349
|
getAssetType: () => getAssetType,
|
|
1264
1350
|
getAttributeDefinition: () => getAttributeDefinition,
|
|
1265
1351
|
getMetamodel: () => getMetamodel,
|
|
1266
1352
|
isMetamodelDocument: () => isMetamodelDocument,
|
|
1353
|
+
readAssetType: () => readAssetType,
|
|
1354
|
+
readMetamodel: () => readMetamodel,
|
|
1267
1355
|
toAssetType: () => toAssetType,
|
|
1268
1356
|
toAttributeDefinition: () => toAttributeDefinition,
|
|
1269
1357
|
toMetamodel: () => toMetamodel
|
|
@@ -1308,12 +1396,36 @@ function toMetamodel(doc) {
|
|
|
1308
1396
|
function isMetamodelDocument(value) {
|
|
1309
1397
|
return !!value && typeof value === "object" && typeof value.Version === "string" && !!value.AssetTypes && typeof value.AssetTypes === "object";
|
|
1310
1398
|
}
|
|
1399
|
+
var PROBE_PATH = "/meta.v1/BaseAsset/Name";
|
|
1400
|
+
async function conditional(transport, path, options) {
|
|
1401
|
+
let etag2 = null, status = 200;
|
|
1402
|
+
const onResponse = (r) => {
|
|
1403
|
+
status = r.status;
|
|
1404
|
+
etag2 = r.headers.get("etag");
|
|
1405
|
+
};
|
|
1406
|
+
const headers = options.ifNoneMatch ? { "if-none-match": options.ifNoneMatch } : void 0;
|
|
1407
|
+
const doc = await transport.request({ method: "GET", path, headers, responseType: "json", onResponse, acceptNotModified: !!options.ifNoneMatch });
|
|
1408
|
+
if (status === 304 || doc === void 0) return null;
|
|
1409
|
+
return { value: doc, etag: etag2 };
|
|
1410
|
+
}
|
|
1311
1411
|
async function getMetamodel(transport) {
|
|
1312
1412
|
return toMetamodel(await transport.request({ method: "GET", path: "/meta.v1", responseType: "json" }));
|
|
1313
1413
|
}
|
|
1414
|
+
async function readMetamodel(transport, options = {}) {
|
|
1415
|
+
const read2 = await conditional(transport, "/meta.v1", options);
|
|
1416
|
+
return read2 && { value: toMetamodel(read2.value), etag: read2.etag };
|
|
1417
|
+
}
|
|
1314
1418
|
async function getAssetType(transport, token) {
|
|
1315
1419
|
return toAssetType(await transport.request({ method: "GET", path: `/meta.v1/${encodeURIComponent(token)}`, responseType: "json" }));
|
|
1316
1420
|
}
|
|
1421
|
+
async function readAssetType(transport, token, options = {}) {
|
|
1422
|
+
const read2 = await conditional(transport, `/meta.v1/${encodeURIComponent(token)}`, options);
|
|
1423
|
+
return read2 && { value: toAssetType(read2.value), etag: read2.etag };
|
|
1424
|
+
}
|
|
1425
|
+
async function etag(transport, options = {}) {
|
|
1426
|
+
const read2 = await conditional(transport, PROBE_PATH, options);
|
|
1427
|
+
return read2 ? read2.etag : options.ifNoneMatch ?? null;
|
|
1428
|
+
}
|
|
1317
1429
|
async function getAttributeDefinition(transport, tokenref) {
|
|
1318
1430
|
const [type, attribute] = tokenref.split(".");
|
|
1319
1431
|
if (!type || !attribute) throw new UsageError(`"${tokenref}" is not an attribute token ("Story.Name")`);
|
|
@@ -1325,6 +1437,13 @@ var TypeCache = class _TypeCache {
|
|
|
1325
1437
|
/** Loaded types; null records that the instance has no such type, so it is not asked for again. */
|
|
1326
1438
|
types = /* @__PURE__ */ new Map();
|
|
1327
1439
|
loading = /* @__PURE__ */ new Map();
|
|
1440
|
+
/** The schema's tag the loaded types came with (meta.v1's ETag, one for the whole schema); null until a load, or for a seeded cache. */
|
|
1441
|
+
etag = null;
|
|
1442
|
+
/** Forget every loaded type (and the tag): the next query loads afresh. In-flight loads finish into the cache as before. */
|
|
1443
|
+
clear() {
|
|
1444
|
+
this.types.clear();
|
|
1445
|
+
this.etag = null;
|
|
1446
|
+
}
|
|
1328
1447
|
/** A cache seeded from a metamodel document. */
|
|
1329
1448
|
static fromMetamodel(metamodel) {
|
|
1330
1449
|
const cache = new _TypeCache();
|
|
@@ -1389,7 +1508,10 @@ var Schema = class _Schema {
|
|
|
1389
1508
|
this.types.set(token, null);
|
|
1390
1509
|
return null;
|
|
1391
1510
|
}
|
|
1392
|
-
const request =
|
|
1511
|
+
const request = readAssetType(this.transport, token).then((read2) => {
|
|
1512
|
+
if (read2?.etag) this.cache.etag = read2.etag;
|
|
1513
|
+
return read2 ? read2.value : null;
|
|
1514
|
+
}).catch((error) => {
|
|
1393
1515
|
if (error instanceof ApiError && error.status === 404) return null;
|
|
1394
1516
|
throw error;
|
|
1395
1517
|
}).then((type) => {
|
|
@@ -1399,6 +1521,23 @@ var Schema = class _Schema {
|
|
|
1399
1521
|
this.loading.set(token, request);
|
|
1400
1522
|
return request;
|
|
1401
1523
|
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Has the instance's schema changed since these types were loaded? One
|
|
1526
|
+
* small conditional read of meta.v1. Unchanged: nothing happens.
|
|
1527
|
+
* Changed (or never tagged, or the transport cannot see headers): the
|
|
1528
|
+
* loaded types are dropped, so the next query loads them again, and the
|
|
1529
|
+
* new tag is kept. Answers whether it changed and the current tag. A
|
|
1530
|
+
* schema without a transport (seeded) never changes.
|
|
1531
|
+
*/
|
|
1532
|
+
async revalidate() {
|
|
1533
|
+
if (!this.transport) return { changed: false, etag: this.cache.etag };
|
|
1534
|
+
const was = this.cache.etag;
|
|
1535
|
+
const now2 = await etag(this.transport, { ifNoneMatch: was });
|
|
1536
|
+
if (was && now2 === was) return { changed: false, etag: was };
|
|
1537
|
+
this.cache.clear();
|
|
1538
|
+
this.cache.etag = now2;
|
|
1539
|
+
return { changed: true, etag: now2 };
|
|
1540
|
+
}
|
|
1402
1541
|
/**
|
|
1403
1542
|
* Load whatever the queries in a batch need to have their rows translated:
|
|
1404
1543
|
* the `from` types, then the types behind every relation hop and sub-select,
|
|
@@ -1459,8 +1598,8 @@ __export(queryV1_exports, {
|
|
|
1459
1598
|
});
|
|
1460
1599
|
function rejected(error) {
|
|
1461
1600
|
const details = error.details;
|
|
1462
|
-
const
|
|
1463
|
-
return
|
|
1601
|
+
const rejections = (details?.exceptions ?? []).filter((e) => e && typeof e.message === "string" && e.message).map((e) => ({ type: typeof e.type === "string" ? e.type : null, message: e.message }));
|
|
1602
|
+
return rejections.length ? new ApiError(rejections.map((r) => r.message).join("; "), error.status, "QUERY_REJECTED", rejections) : error;
|
|
1464
1603
|
}
|
|
1465
1604
|
async function queries2(transport, qs, options = {}) {
|
|
1466
1605
|
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`);
|
|
@@ -1493,6 +1632,8 @@ __export(rest1_exports, {
|
|
|
1493
1632
|
history: () => history,
|
|
1494
1633
|
list: () => list2,
|
|
1495
1634
|
momentless: () => momentless,
|
|
1635
|
+
operate: () => operate,
|
|
1636
|
+
template: () => template,
|
|
1496
1637
|
toQuery: () => toQuery
|
|
1497
1638
|
});
|
|
1498
1639
|
var toRef = (doc) => ({ _oid: momentless(doc.idref) });
|
|
@@ -1547,6 +1688,19 @@ async function get2(transport, oid, select) {
|
|
|
1547
1688
|
const doc = await transport.request({ method: "GET", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { sel: select?.join(",") }, responseType: "json" });
|
|
1548
1689
|
return fromAssetDocument(doc);
|
|
1549
1690
|
}
|
|
1691
|
+
async function operate(transport, oid, op) {
|
|
1692
|
+
const [type, id] = momentless(oid).split(":");
|
|
1693
|
+
if (!type || !id) throw new UsageError(`"${oid}" is not an OID token (Story:1234)`);
|
|
1694
|
+
const doc = await transport.request({ method: "POST", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { op }, responseType: "json" });
|
|
1695
|
+
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);
|
|
1696
|
+
const moment = doc.id.split(":")[2] ?? null;
|
|
1697
|
+
return { oid: momentless(doc.id), moment };
|
|
1698
|
+
}
|
|
1699
|
+
async function template(transport, type, options = {}) {
|
|
1700
|
+
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" });
|
|
1701
|
+
const { _oid: _ignored, ...values } = fromAssetDocument({ ...doc, id: doc.id || `${type}:0` });
|
|
1702
|
+
return values;
|
|
1703
|
+
}
|
|
1550
1704
|
var history = (transport, target, params) => {
|
|
1551
1705
|
const [type, id] = target.split(":");
|
|
1552
1706
|
const path = id ? `/rest-1.v1/Hist/${encodeURIComponent(type)}/${encodeURIComponent(id)}` : `/rest-1.v1/Hist/${encodeURIComponent(type)}`;
|
|
@@ -1629,11 +1783,21 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1629
1783
|
return row;
|
|
1630
1784
|
}
|
|
1631
1785
|
// ---- the endpoints behind the reads -----------------------------------------------
|
|
1632
|
-
/**
|
|
1786
|
+
/**
|
|
1787
|
+
* meta.v1: the instance's schema, raw. (The translated, cached view is
|
|
1788
|
+
* `schema`; `schema.revalidate()` drops it when the schema's tag moves.)
|
|
1789
|
+
* `read` and `readType` are conditional: pass the tag a previous answer
|
|
1790
|
+
* carried and an unchanged schema answers null without a download —
|
|
1791
|
+
* meta.v1 sends one strong ETag for the whole schema. `etag()` reads that
|
|
1792
|
+
* tag alone, from the smallest resource.
|
|
1793
|
+
*/
|
|
1633
1794
|
meta = {
|
|
1634
1795
|
metamodel: () => getMetamodel(this.transport),
|
|
1796
|
+
read: (options) => readMetamodel(this.transport, options),
|
|
1635
1797
|
assetType: (token) => getAssetType(this.transport, token),
|
|
1636
|
-
|
|
1798
|
+
readType: (token, options) => readAssetType(this.transport, token, options),
|
|
1799
|
+
attributeDefinition: (tokenref) => getAttributeDefinition(this.transport, tokenref),
|
|
1800
|
+
etag: (options) => etag(this.transport, options)
|
|
1637
1801
|
};
|
|
1638
1802
|
/**
|
|
1639
1803
|
* loc-2.v1: labels for tags, in one request (meta's DisplayName values are
|
|
@@ -1644,16 +1808,39 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1644
1808
|
return localize(this.transport, tags, options);
|
|
1645
1809
|
}
|
|
1646
1810
|
/**
|
|
1647
|
-
* rest-1.v1, for what
|
|
1648
|
-
* true`), one asset by OID without a page,
|
|
1649
|
-
* moment)
|
|
1811
|
+
* rest-1.v1, for what the other endpoints cannot do: deleted assets
|
|
1812
|
+
* (`deleted: true`), one asset by OID without a page, history (one row
|
|
1813
|
+
* per moment), a new asset's template (`template`: what the type's
|
|
1814
|
+
* initializer fills in for a context — a Timebox's next name and dates,
|
|
1815
|
+
* a Story's carry-over from a Request), and an operation's result
|
|
1816
|
+
* (`operate`: api/asset performs a Copy but never says what it made;
|
|
1817
|
+
* rest-1 answers the new OID). Parameters are spelled as a query's
|
|
1818
|
+
* (`select`, `filter`).
|
|
1650
1819
|
*/
|
|
1651
1820
|
data = {
|
|
1652
1821
|
list: (type, params) => list2(this.transport, type, params),
|
|
1653
1822
|
get: (oid, select) => get2(this.transport, oid, select),
|
|
1654
|
-
history: (target, params) => history(this.transport, target, params)
|
|
1823
|
+
history: (target, params) => history(this.transport, target, params),
|
|
1824
|
+
template: (type, options) => template(this.transport, type, options),
|
|
1825
|
+
operate: (oid, op) => operate(this.transport, oid, op)
|
|
1655
1826
|
};
|
|
1656
|
-
/**
|
|
1827
|
+
/** 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. */
|
|
1828
|
+
async copy(oid) {
|
|
1829
|
+
return (await operate(this.transport, oid, "Copy")).oid;
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* What a form needs for an asset type in a scope: the attributes the
|
|
1833
|
+
* server requires to create one (meta), the ones the product shows there
|
|
1834
|
+
* (api/visibleFields, custom fields included once configured), and the
|
|
1835
|
+
* scope's scheme's important fields and whether they are enforced at
|
|
1836
|
+
* close. `visible` alone answers for several types and scopes at once.
|
|
1837
|
+
*/
|
|
1838
|
+
fields = {
|
|
1839
|
+
describe: (type, scope) => describe2(this.transport, this.schema, type, scope),
|
|
1840
|
+
visible: (types, scopes) => visibleFields(this.transport, types, scopes),
|
|
1841
|
+
visibleByAsset: (oid) => visibleFieldsByAsset(this.transport, oid)
|
|
1842
|
+
};
|
|
1843
|
+
/** @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. */
|
|
1657
1844
|
queryV1 = {
|
|
1658
1845
|
query: (q) => this.query(q),
|
|
1659
1846
|
queries: (qs) => this.queries(qs)
|
|
@@ -1685,7 +1872,7 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1685
1872
|
await this.schema.ensureFor(commands);
|
|
1686
1873
|
return sendGuarded(this.transport, commands, { ...options, lookup: this.schema });
|
|
1687
1874
|
},
|
|
1688
|
-
/** @deprecated since 1.0.0-alpha.3: use `client.query` (or `client.queryPage`
|
|
1875
|
+
/** @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. */
|
|
1689
1876
|
query: async (q) => {
|
|
1690
1877
|
await this.schema.ensureFor([q]);
|
|
1691
1878
|
return query(this.transport, q, { lookup: this.schema });
|
|
@@ -1745,8 +1932,19 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1745
1932
|
};
|
|
1746
1933
|
/** Where a browser opens things on this instance: an asset's page, an attachment's bytes, an embedded image. */
|
|
1747
1934
|
urls = {
|
|
1748
|
-
/**
|
|
1749
|
-
|
|
1935
|
+
/**
|
|
1936
|
+
* The asset's page, from an OID token or an asset Number (S-01001,
|
|
1937
|
+
* what a person typed and what a message shows): assetdetail.v1
|
|
1938
|
+
* resolves either and redirects to the type's own detail page; a signed-
|
|
1939
|
+
* out reader lands there after logging in. Anything else is a UsageError.
|
|
1940
|
+
*/
|
|
1941
|
+
asset: (ref) => {
|
|
1942
|
+
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
1943
|
+
const text = ref.trim();
|
|
1944
|
+
if (isOidToken(text)) return `${base}/assetdetail.v1?oid=${momentless(text)}`;
|
|
1945
|
+
if (isAssetNumber(text)) return `${base}/assetdetail.v1?Number=${encodeURIComponent(text)}`;
|
|
1946
|
+
throw new UsageError(`"${ref}" is neither an OID token (Story:1234) nor an asset number (S-1234)`);
|
|
1947
|
+
},
|
|
1750
1948
|
attachment: (id) => attachmentUrl(this.config.baseUrl, id),
|
|
1751
1949
|
embeddedImage: (id) => embeddedImageUrl(this.config.baseUrl, id)
|
|
1752
1950
|
};
|
|
@@ -1775,7 +1973,9 @@ var AgilityApiClient = class _AgilityApiClient {
|
|
|
1775
1973
|
attachment,
|
|
1776
1974
|
createFetchTransport,
|
|
1777
1975
|
describeBatch,
|
|
1976
|
+
fields,
|
|
1778
1977
|
guard,
|
|
1978
|
+
importantName,
|
|
1779
1979
|
isAssetNumber,
|
|
1780
1980
|
isOidToken,
|
|
1781
1981
|
kindOf,
|