@versionone/agility-sdk 1.0.0-alpha.0 → 1.0.0-alpha.2
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 +25 -7
- package/dist/index.cjs +97 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -5
- package/dist/index.d.ts +39 -5
- package/dist/index.js +97 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ npm test
|
|
|
44
44
|
- `assets.sendGuarded` - a batch as a compare-and-set on each asset's ChangeDate; changed assets come back `skipped`
|
|
45
45
|
- `attachments.list` / `page` / `info` / `get` / `add` / `put` / `remove` / `url` - files attached to an asset: metadata on the Attachment asset, bytes through attachment.img; `page` adds the total and whether more pages follow; `get`'s `contentType` is the type the server sent the bytes with; `add` sends a small file inline (Base64, one request) and a large one in two steps (create, then upload), and a failed upload throws `AttachmentUploadError` naming the empty attachment it left
|
|
46
46
|
- `embeddedImages.upload` / `url` / `normalize` - images in rich text: upload returns the `<img src>` URL; `normalize` rewrites `embedded.img` references to this instance's URL. They cannot be deleted
|
|
47
|
-
- `batch.run` / `batch.reversible` - preview, snapshot, (confirm,) send guarded, and a receipt with a reverse batch. Undo (`batch.reversible`) reactivates a closed asset before deleting it, and a second command on one asset is sent as a second guarded pass. `confirm` sees the touched assets' names; aliased updates/executes are sent as written with `guard: false`; a failure after the send throws `BatchRecordError` carrying the receipt
|
|
47
|
+
- `batch.run` / `batch.reversible` - preview, snapshot, (confirm,) send guarded, and a receipt with a reverse batch. Undo (`batch.reversible`) reactivates a closed asset before deleting it, and a second command on one asset is sent as a second guarded pass. The reverse uses only operations the type has in meta (a created Conversation, which has no `Delete`, is a note), and undoing an `Inactivate` or `QuickClose` reopens the descendants that close cascaded to (`receipt.openDescendants`, `receipt.cascaded`), parent first. A command the server reports as failed lands in `receipt.failed`, not `skipped`. `confirm` sees the touched assets' names; aliased updates/executes are sent as written with `guard: false`; a failure after the send throws `BatchRecordError` carrying the receipt
|
|
48
48
|
- `TypeCache` - one instance's loaded type definitions, shared by many clients (`new AgilityApiClient(config, { types })`): a server that builds a client per user fetches each type once
|
|
49
49
|
- `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
50
|
- `ApiError` - typed API error; `CommandFailureError` - an api/asset batch in which commands failed (`failures` by index, plus the `response`)
|
|
@@ -71,15 +71,33 @@ prerelease (`1.0.0-alpha.0`, `-beta.N`, `-rc.N`) goes out on its own
|
|
|
71
71
|
dist-tag (`alpha`, `beta`, `rc`), so `latest` is never a prerelease.
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
npm version 1.0.0-alpha.
|
|
75
|
-
git tag v1.0.0-alpha.
|
|
74
|
+
npm version 1.0.0-alpha.2 --no-git-tag-version # bump, commit, merge
|
|
75
|
+
git tag v1.0.0-alpha.2 && git push origin v1.0.0-alpha.2
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
The workflow does not publish directly: it runs `npm stage publish`, which
|
|
79
|
+
puts the version in the registry where nobody can install it yet, and prints
|
|
80
|
+
its stage ID in the run's summary. A maintainer then approves it with 2FA,
|
|
81
|
+
which is what publishes it:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm stage view <stage-id> # what was staged
|
|
85
|
+
npm stage approve <stage-id> # publish it (asks for 2FA)
|
|
86
|
+
npm stage reject <stage-id> # discard it
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
or approves it on the package's page on npmjs. `npm stage` needs npm
|
|
90
|
+
11.15.0 or later.
|
|
91
|
+
|
|
78
92
|
Authentication is npm trusted publishing: on npmjs, the package's settings
|
|
79
|
-
link this repository
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
link this repository, `publish.yml` and the GitHub environment `npm`, with
|
|
94
|
+
the `npm stage publish` permission (leave plain `npm publish` off, as npm
|
|
95
|
+
recommends), and no token is stored. A trusted-publisher link expires; an
|
|
96
|
+
expired one must be deleted and created again, and until then the workflow
|
|
97
|
+
fails with `ENEEDAUTH`. The `npm` environment (repository settings) requires
|
|
98
|
+
an approval, so a pushed tag waits for a maintainer before it stages. A
|
|
99
|
+
package must exist before it can be linked, so the very first version was
|
|
100
|
+
published once by hand by an `@versionone` org member.
|
|
83
101
|
|
|
84
102
|
## Before PR
|
|
85
103
|
|
package/dist/index.cjs
CHANGED
|
@@ -889,9 +889,46 @@ async function snapshot(transport, described) {
|
|
|
889
889
|
}
|
|
890
890
|
return before;
|
|
891
891
|
}
|
|
892
|
-
var INVERSE = { Inactivate: "Reactivate", Reactivate: "Inactivate", Delete: "Undelete", Undelete: "Delete" };
|
|
893
|
-
|
|
892
|
+
var INVERSE = { Inactivate: "Reactivate", QuickClose: "Reactivate", Reactivate: "Inactivate", Delete: "Undelete", Undelete: "Delete" };
|
|
893
|
+
var CASCADING = /* @__PURE__ */ new Set(["Inactivate", "QuickClose"]);
|
|
894
|
+
var CASCADE_DEPTH = 8;
|
|
895
|
+
async function openDescendantsOf(transport, schema, roots) {
|
|
896
|
+
const out = {};
|
|
897
|
+
const rootOf = new Map(roots.map((r) => [momentless(r), momentless(r)]));
|
|
898
|
+
const seen = new Set(rootOf.keys());
|
|
899
|
+
let frontier = [...rootOf.keys()];
|
|
900
|
+
for (let depth = 0; depth < CASCADE_DEPTH && frontier.length; depth += 1) {
|
|
901
|
+
const children = /* @__PURE__ */ new Map();
|
|
902
|
+
for (const [type, list3] of byType(frontier)) {
|
|
903
|
+
await schema.load(type);
|
|
904
|
+
if (!schema.attribute(type, "Children")) continue;
|
|
905
|
+
for (const part of chunks(list3)) {
|
|
906
|
+
const rows = await query(transport, { from: type, select: ["Children"], filter: ["ID=$oids"], with: { $oids: part }, page: { size: part.length, start: 0 } });
|
|
907
|
+
for (const r of rows) {
|
|
908
|
+
const parent = momentless(r._oid);
|
|
909
|
+
for (const c of [].concat(plain(r.Children) ?? [])) {
|
|
910
|
+
const child = momentless(String(c));
|
|
911
|
+
if (seen.has(child)) continue;
|
|
912
|
+
seen.add(child);
|
|
913
|
+
children.set(child, rootOf.get(parent));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const states = children.size ? await statesOf(transport, [...children.keys()]) : {};
|
|
919
|
+
frontier = [];
|
|
920
|
+
for (const [child, root] of children) {
|
|
921
|
+
if (states[child] === void 0 || isClosed(states[child])) continue;
|
|
922
|
+
rootOf.set(child, root);
|
|
923
|
+
(out[root] ??= []).push(child);
|
|
924
|
+
frontier.push(child);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
function reverseOf(described, before, done, context = {}) {
|
|
894
930
|
const commands = [], notes = [];
|
|
931
|
+
const cannot = (oid, op) => context.canOperate?.(typeOfOid(oid), op) === false;
|
|
895
932
|
const modified = new Set(done.modified.map(momentless)), operated = new Set(done.operated.map(momentless));
|
|
896
933
|
for (const u of described.updates) {
|
|
897
934
|
if (!modified.has(u.oid)) continue;
|
|
@@ -916,10 +953,32 @@ function reverseOf(described, before, done) {
|
|
|
916
953
|
for (const e of described.executes) {
|
|
917
954
|
if (!operated.has(e.oid)) continue;
|
|
918
955
|
const inverse = INVERSE[e.op];
|
|
919
|
-
if (inverse)
|
|
920
|
-
|
|
956
|
+
if (!inverse) {
|
|
957
|
+
notes.push(`${e.oid}: ${e.op} has no known inverse (was ${String(before[e.oid]?.AssetState ?? "?")})`);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (cannot(e.oid, inverse)) {
|
|
961
|
+
notes.push(`${e.oid}: ${typeOfOid(e.oid)} has no ${inverse} operation, so its ${e.op} is not reversed`);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
commands.push({ from: e.oid, execute: inverse });
|
|
965
|
+
if (!CASCADING.has(e.op)) continue;
|
|
966
|
+
const open = context.openDescendants?.[e.oid] ?? [];
|
|
967
|
+
const closed = context.cascaded ? context.cascaded[e.oid] ?? [] : void 0;
|
|
968
|
+
if (closed === void 0) {
|
|
969
|
+
if (open.length) notes.push(`${e.oid}: ${e.op} may also have closed ${open.length} open descendant${open.length === 1 ? "" : "s"} (${open.join(", ")}); they could not be read back, so they are not reopened`);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
for (const d of closed) {
|
|
973
|
+
if (cannot(d, "Reactivate")) notes.push(`${d}: closed with ${e.oid}, but ${typeOfOid(d)} has no Reactivate operation`);
|
|
974
|
+
else commands.push({ from: d, execute: "Reactivate" });
|
|
975
|
+
}
|
|
976
|
+
if (closed.length) notes.push(`${e.oid}: ${e.op} also closed ${closed.join(", ")}; reopened after it`);
|
|
977
|
+
}
|
|
978
|
+
for (const oid of [...done.created].reverse().map(momentless)) {
|
|
979
|
+
if (cannot(oid, "Delete")) notes.push(`${oid}: ${typeOfOid(oid)} has no Delete operation, so it is not deleted${typeOfOid(oid) === "Conversation" ? " (the server removes a Conversation once its last Expression is deleted)" : ""}`);
|
|
980
|
+
else commands.push({ from: oid, execute: "Delete" });
|
|
921
981
|
}
|
|
922
|
-
for (const oid of [...done.created].reverse()) commands.push({ from: momentless(oid), execute: "Delete" });
|
|
923
982
|
for (const d of described.deferred ?? []) notes.push(`command ${d.index} (${d.alias}): an aliased ${d.kind} of ${d.from}, sent as written with no snapshot; not reversed`);
|
|
924
983
|
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");
|
|
925
984
|
return { commands, notes };
|
|
@@ -954,12 +1013,14 @@ async function runBatch(transport, schema, commands, options = {}) {
|
|
|
954
1013
|
const described = describe(commands, projected);
|
|
955
1014
|
const touched = touchedBy(projected);
|
|
956
1015
|
const names = await namesOf(transport, schema, touched, noteFailure);
|
|
957
|
-
const receipt = { receipt: 1, request: commands, previewedAt, preview: projected, described, names: Object.fromEntries(names), before: {}, sent: false, guarded: false, created: [], touched: [], after: {}, skipped: [], reverse: { commands: [], notes: [] }, notes };
|
|
1016
|
+
const receipt = { receipt: 1, request: commands, previewedAt, preview: projected, described, names: Object.fromEntries(names), before: {}, sent: false, guarded: false, created: [], touched: [], after: {}, skipped: [], failed: [], openDescendants: {}, cascaded: {}, reverse: { commands: [], notes: [] }, notes };
|
|
958
1017
|
const nothing = !described.creates.length && !described.updates.length && !described.executes.length && !described.queries.length && !described.deferred.length;
|
|
959
1018
|
if (nothing) return receipt;
|
|
960
1019
|
for (const d of described.deferred) notes.push(`command ${d.index} (${d.alias}): an aliased ${d.kind}, which preview does not expand; sent as written, not snapshotted, not reversible`);
|
|
961
1020
|
receipt.snapshotAt = now();
|
|
962
1021
|
receipt.before = await snapshot(transport, described);
|
|
1022
|
+
const closing = described.executes.filter((e) => CASCADING.has(e.op)).map((e) => e.oid);
|
|
1023
|
+
if (closing.length) receipt.openDescendants = await openDescendantsOf(transport, schema, closing);
|
|
963
1024
|
if (options.dryRun) return receipt;
|
|
964
1025
|
if (options.confirm && !await options.confirm(receipt)) return receipt;
|
|
965
1026
|
let unstamped = [];
|
|
@@ -991,13 +1052,29 @@ async function runBatch(transport, schema, commands, options = {}) {
|
|
|
991
1052
|
const unnamed = { number: null, name: null };
|
|
992
1053
|
receipt.created = done.created.map((oid, i) => ({ alias: described.creates[i]?.alias ?? null, type: described.creates[i]?.type ?? typeOfOid(oid), oid, ...unnamed }));
|
|
993
1054
|
receipt.touched = hit.map((oid) => ({ oid, ...names.get(oid) ?? unnamed }));
|
|
994
|
-
|
|
995
|
-
|
|
1055
|
+
const sentAs = receipt.sentCommands ?? [];
|
|
1056
|
+
const oidOfSent = (i) => {
|
|
1057
|
+
const body = i === null ? void 0 : sentAs[i] ? bodyOf3(sentAs[i]) : void 0;
|
|
1058
|
+
const id = body?.with?.$oid ?? body?.from;
|
|
1059
|
+
return typeof id === "string" && id.includes(":") ? momentless(id) : null;
|
|
1060
|
+
};
|
|
1061
|
+
receipt.failed = [...new Set(done.failures.map((f) => oidOfSent(f.index)).filter((oid) => !!oid && touched.includes(oid) && !hit.includes(oid)))];
|
|
1062
|
+
receipt.skipped = useGuard ? [.../* @__PURE__ */ new Set([...unstamped, ...touched.filter((oid) => !hit.includes(oid) && !receipt.failed.includes(oid))])] : [];
|
|
1063
|
+
for (const type of new Set([...done.created, ...hit].map((oid) => typeOfOid(momentless(oid))))) await schema.load(type).catch(() => null);
|
|
1064
|
+
const canOperate = (type, op) => schema.canOperate(type, op);
|
|
1065
|
+
const operatedNow = new Set(done.operated.map(momentless));
|
|
1066
|
+
const closed = Object.keys(receipt.openDescendants).filter((oid) => operatedNow.has(oid) && receipt.openDescendants[oid].length);
|
|
1067
|
+
receipt.reverse = reverseOf(described, receipt.before, done, { openDescendants: receipt.openDescendants, cascaded: closed.length ? void 0 : {}, canOperate });
|
|
996
1068
|
try {
|
|
1069
|
+
if (closed.length) {
|
|
1070
|
+
const states = await statesOf(transport, closed.flatMap((oid) => receipt.openDescendants[oid]));
|
|
1071
|
+
receipt.cascaded = Object.fromEntries(closed.map((oid) => [oid, receipt.openDescendants[oid].filter((d) => isClosed(states[d]))]));
|
|
1072
|
+
receipt.reverse = reverseOf(described, receipt.before, done, { openDescendants: receipt.openDescendants, cascaded: receipt.cascaded, canOperate });
|
|
1073
|
+
}
|
|
997
1074
|
const more = await namesOf(transport, schema, [...done.created, ...hit.filter((oid) => !names.has(oid))], noteFailure);
|
|
998
1075
|
receipt.created = receipt.created.map((c) => ({ ...c, ...more.get(c.oid) ?? unnamed }));
|
|
999
1076
|
receipt.touched = receipt.touched.map((t) => names.has(t.oid) ? t : { ...t, ...more.get(t.oid) ?? unnamed });
|
|
1000
|
-
const all = [...done.created, ...hit];
|
|
1077
|
+
const all = [.../* @__PURE__ */ new Set([...done.created, ...hit, ...Object.values(receipt.cascaded).flat()])];
|
|
1001
1078
|
receipt.after = all.length ? await stampsOf(transport, all) : {};
|
|
1002
1079
|
} catch (error) {
|
|
1003
1080
|
throw new BatchRecordError(error, receipt);
|
|
@@ -1161,7 +1238,8 @@ function toAssetType(doc) {
|
|
|
1161
1238
|
...doc.DefaultOrderBy ? { DefaultOrderBy: doc.DefaultOrderBy.tokenref } : {},
|
|
1162
1239
|
...doc.DefaultDisplayBy ? { DefaultDisplayBy: doc.DefaultDisplayBy.tokenref } : {},
|
|
1163
1240
|
...doc.ShortName ? { ShortName: doc.ShortName.tokenref } : {},
|
|
1164
|
-
Attributes: Object.fromEntries(Object.entries(doc.Attributes ?? {}).map(([key, value]) => [key, toAttributeDefinition(value)]))
|
|
1241
|
+
Attributes: Object.fromEntries(Object.entries(doc.Attributes ?? {}).map(([key, value]) => [key, toAttributeDefinition(value)])),
|
|
1242
|
+
...doc.Operations ? { Operations: Object.keys(doc.Operations).map((token) => token.slice(token.indexOf(".") + 1)) } : {}
|
|
1165
1243
|
};
|
|
1166
1244
|
}
|
|
1167
1245
|
function toMetamodel(doc) {
|
|
@@ -1225,6 +1303,15 @@ var Schema = class _Schema {
|
|
|
1225
1303
|
attribute(type, name) {
|
|
1226
1304
|
return attributeOf(this, type, name);
|
|
1227
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Can the type do this operation (Delete, Reactivate, …)? Undefined when
|
|
1308
|
+
* the type is not loaded or its definition carries no operations, so a
|
|
1309
|
+
* caller can tell "no" from "don't know".
|
|
1310
|
+
*/
|
|
1311
|
+
canOperate(type, op) {
|
|
1312
|
+
const t = this.assetType(type);
|
|
1313
|
+
return t?.Operations ? t.Operations.includes(op) : void 0;
|
|
1314
|
+
}
|
|
1228
1315
|
/** The tokens loaded so far. */
|
|
1229
1316
|
get loaded() {
|
|
1230
1317
|
return [...this.types.entries()].filter(([, t]) => t !== null).map(([token]) => token);
|