@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/dist/index.d.cts CHANGED
@@ -301,6 +301,8 @@ interface AssetType {
301
301
  /** The attribute that carries the short name (Number on workitems). */
302
302
  ShortName?: AttributeTokenRef;
303
303
  Attributes: Record<AttributeToken, AttributeDefinition>;
304
+ /** The operations the type supports, by bare name (Delete, Inactivate, …); delivered keyed by token (Story.Delete). */
305
+ Operations?: string[];
304
306
  }
305
307
  type AssetTypeMap$1 = Record<AssetTypeToken, AssetType>;
306
308
  interface Metamodel {
@@ -772,6 +774,12 @@ declare class Schema implements AttributeLookup {
772
774
  assetType(token: AssetTypeToken): AssetType | null | undefined;
773
775
  /** An attribute by bare name; undefined when the type is not loaded, null when it has no such attribute. */
774
776
  attribute(type: AssetTypeToken, name: string): AttributeDefinition | null | undefined;
777
+ /**
778
+ * Can the type do this operation (Delete, Reactivate, …)? Undefined when
779
+ * the type is not loaded or its definition carries no operations, so a
780
+ * caller can tell "no" from "don't know".
781
+ */
782
+ canOperate(type: AssetTypeToken, op: string): boolean | undefined;
775
783
  /** The tokens loaded so far. */
776
784
  get loaded(): AssetTypeToken[];
777
785
  /**
@@ -803,6 +811,12 @@ declare class Schema implements AttributeLookup {
803
811
  * which were skipped by the guard; and a reverse batch that undoes what
804
812
  * succeeded, best effort, with a note for anything it cannot.
805
813
  *
814
+ * Closing a workitem cascades: Core's Inactivate and QuickClose also close
815
+ * its open descendants, and Reactivate reopens only the asset it names. So
816
+ * before a batch that closes, the receipt records each closing target's
817
+ * open descendants; after it, the ones the close actually closed; and the
818
+ * reverse reopens the parent, then those, top down.
819
+ *
806
820
  * The snapshot keeps the endpoint's spelling (strings, relations as OID
807
821
  * tokens) on purpose: it is what a reverse batch writes back.
808
822
  */
@@ -867,18 +881,29 @@ interface Reverse {
867
881
  commands: Command[];
868
882
  notes: string[];
869
883
  }
884
+ /** What reverseOf can know beyond the response, each optional. */
885
+ interface ReverseContext {
886
+ /** Per closing target, its open descendants before the batch (Receipt.openDescendants). */
887
+ openDescendants?: Record<Oid, Oid[]>;
888
+ /** Per closing target, the descendants the close closed (Receipt.cascaded); undefined when they could not be read back. */
889
+ cascaded?: Record<Oid, Oid[]>;
890
+ /** Can this type do this operation? Undefined is "don't know", and the command is kept. */
891
+ canOperate?: (type: AssetTypeToken, op: string) => boolean | undefined;
892
+ }
870
893
  /**
871
894
  * A batch that undoes what the response says succeeded — best effort. Only
872
895
  * what the response confirms is reversed (membership in it, not command
873
896
  * index, decides): updated attributes put back from the snapshot, the
874
- * inverse operation where one is known, created assets deleted (newest
875
- * first).
897
+ * inverse operation where one is known (and, for a close, the descendants
898
+ * it closed reopened after it, top down), created assets deleted (newest
899
+ * first). An operation a type does not have, by `context.canOperate`, is
900
+ * a note rather than a command the server would refuse.
876
901
  */
877
902
  declare function reverseOf(described: Described, before: Record<Oid, Record<string, unknown>>, done: {
878
903
  created: Oid[];
879
904
  modified: Oid[];
880
905
  operated: Oid[];
881
- }): Reverse;
906
+ }, context?: ReverseContext): Reverse;
882
907
  interface Receipt {
883
908
  receipt: 1;
884
909
  request: Command[];
@@ -908,8 +933,14 @@ interface Receipt {
908
933
  } & Named)[];
909
934
  /** ChangeDate of everything the batch touched, just after it: the stamp a later reverse checks for drift. */
910
935
  after: Record<Oid, string>;
911
- /** Touched assets the guard left alone: changed or gone since the snapshot. */
936
+ /** Touched assets the guard left alone: changed or gone since the snapshot. Not the ones whose command failed: those are in `failed`. */
912
937
  skipped: Oid[];
938
+ /** Touched assets whose command the server reports as failed (outcome.failures says why). */
939
+ failed: Oid[];
940
+ /** Per asset the batch closes (Inactivate, QuickClose): its open descendants at the snapshot, top down. */
941
+ openDescendants: Record<Oid, Oid[]>;
942
+ /** Per asset the batch closed: the descendants the close closed with it, read back after the send. */
943
+ cascaded: Record<Oid, Oid[]>;
913
944
  reverse: Reverse;
914
945
  /** What the record could not do and why: a name lookup that failed, deferred commands sent unsnapshotted. */
915
946
  notes: string[];
@@ -985,7 +1016,10 @@ declare function reversible(transport: Transport, receipt: Pick<Receipt, "revers
985
1016
  * pattern first, so it cannot carry grammar. The ID stays a variable.
986
1017
  * - Moment is NOT a stamp: `Moment=<an earlier moment of this asset>`
987
1018
  * selects the asset as it was then (as-of), and an update scoped by it
988
- * lands on the current asset. A Moment guard guards nothing.
1019
+ * lands on the current asset. A Moment guard guards nothing. Proven for
1020
+ * writes on 2026-11-11: an update filtered on a stale Moment previews as
1021
+ * one command and applies, overwriting the change made in between
1022
+ * ("why not Moment, on a write"; versionone/VersionOne.SDK.JavaScript#51).
989
1023
  * Every type has ChangeDate except the Ideas product's (Idea*), which keeps
990
1024
  * no history and cannot be guarded.
991
1025
  *
package/dist/index.d.ts CHANGED
@@ -301,6 +301,8 @@ interface AssetType {
301
301
  /** The attribute that carries the short name (Number on workitems). */
302
302
  ShortName?: AttributeTokenRef;
303
303
  Attributes: Record<AttributeToken, AttributeDefinition>;
304
+ /** The operations the type supports, by bare name (Delete, Inactivate, …); delivered keyed by token (Story.Delete). */
305
+ Operations?: string[];
304
306
  }
305
307
  type AssetTypeMap$1 = Record<AssetTypeToken, AssetType>;
306
308
  interface Metamodel {
@@ -772,6 +774,12 @@ declare class Schema implements AttributeLookup {
772
774
  assetType(token: AssetTypeToken): AssetType | null | undefined;
773
775
  /** An attribute by bare name; undefined when the type is not loaded, null when it has no such attribute. */
774
776
  attribute(type: AssetTypeToken, name: string): AttributeDefinition | null | undefined;
777
+ /**
778
+ * Can the type do this operation (Delete, Reactivate, …)? Undefined when
779
+ * the type is not loaded or its definition carries no operations, so a
780
+ * caller can tell "no" from "don't know".
781
+ */
782
+ canOperate(type: AssetTypeToken, op: string): boolean | undefined;
775
783
  /** The tokens loaded so far. */
776
784
  get loaded(): AssetTypeToken[];
777
785
  /**
@@ -803,6 +811,12 @@ declare class Schema implements AttributeLookup {
803
811
  * which were skipped by the guard; and a reverse batch that undoes what
804
812
  * succeeded, best effort, with a note for anything it cannot.
805
813
  *
814
+ * Closing a workitem cascades: Core's Inactivate and QuickClose also close
815
+ * its open descendants, and Reactivate reopens only the asset it names. So
816
+ * before a batch that closes, the receipt records each closing target's
817
+ * open descendants; after it, the ones the close actually closed; and the
818
+ * reverse reopens the parent, then those, top down.
819
+ *
806
820
  * The snapshot keeps the endpoint's spelling (strings, relations as OID
807
821
  * tokens) on purpose: it is what a reverse batch writes back.
808
822
  */
@@ -867,18 +881,29 @@ interface Reverse {
867
881
  commands: Command[];
868
882
  notes: string[];
869
883
  }
884
+ /** What reverseOf can know beyond the response, each optional. */
885
+ interface ReverseContext {
886
+ /** Per closing target, its open descendants before the batch (Receipt.openDescendants). */
887
+ openDescendants?: Record<Oid, Oid[]>;
888
+ /** Per closing target, the descendants the close closed (Receipt.cascaded); undefined when they could not be read back. */
889
+ cascaded?: Record<Oid, Oid[]>;
890
+ /** Can this type do this operation? Undefined is "don't know", and the command is kept. */
891
+ canOperate?: (type: AssetTypeToken, op: string) => boolean | undefined;
892
+ }
870
893
  /**
871
894
  * A batch that undoes what the response says succeeded — best effort. Only
872
895
  * what the response confirms is reversed (membership in it, not command
873
896
  * index, decides): updated attributes put back from the snapshot, the
874
- * inverse operation where one is known, created assets deleted (newest
875
- * first).
897
+ * inverse operation where one is known (and, for a close, the descendants
898
+ * it closed reopened after it, top down), created assets deleted (newest
899
+ * first). An operation a type does not have, by `context.canOperate`, is
900
+ * a note rather than a command the server would refuse.
876
901
  */
877
902
  declare function reverseOf(described: Described, before: Record<Oid, Record<string, unknown>>, done: {
878
903
  created: Oid[];
879
904
  modified: Oid[];
880
905
  operated: Oid[];
881
- }): Reverse;
906
+ }, context?: ReverseContext): Reverse;
882
907
  interface Receipt {
883
908
  receipt: 1;
884
909
  request: Command[];
@@ -908,8 +933,14 @@ interface Receipt {
908
933
  } & Named)[];
909
934
  /** ChangeDate of everything the batch touched, just after it: the stamp a later reverse checks for drift. */
910
935
  after: Record<Oid, string>;
911
- /** Touched assets the guard left alone: changed or gone since the snapshot. */
936
+ /** Touched assets the guard left alone: changed or gone since the snapshot. Not the ones whose command failed: those are in `failed`. */
912
937
  skipped: Oid[];
938
+ /** Touched assets whose command the server reports as failed (outcome.failures says why). */
939
+ failed: Oid[];
940
+ /** Per asset the batch closes (Inactivate, QuickClose): its open descendants at the snapshot, top down. */
941
+ openDescendants: Record<Oid, Oid[]>;
942
+ /** Per asset the batch closed: the descendants the close closed with it, read back after the send. */
943
+ cascaded: Record<Oid, Oid[]>;
913
944
  reverse: Reverse;
914
945
  /** What the record could not do and why: a name lookup that failed, deferred commands sent unsnapshotted. */
915
946
  notes: string[];
@@ -985,7 +1016,10 @@ declare function reversible(transport: Transport, receipt: Pick<Receipt, "revers
985
1016
  * pattern first, so it cannot carry grammar. The ID stays a variable.
986
1017
  * - Moment is NOT a stamp: `Moment=<an earlier moment of this asset>`
987
1018
  * selects the asset as it was then (as-of), and an update scoped by it
988
- * lands on the current asset. A Moment guard guards nothing.
1019
+ * lands on the current asset. A Moment guard guards nothing. Proven for
1020
+ * writes on 2026-11-11: an update filtered on a stale Moment previews as
1021
+ * one command and applies, overwriting the change made in between
1022
+ * ("why not Moment, on a write"; versionone/VersionOne.SDK.JavaScript#51).
989
1023
  * Every type has ChangeDate except the Ideas product's (Idea*), which keeps
990
1024
  * no history and cannot be guarded.
991
1025
  *
package/dist/index.js CHANGED
@@ -827,9 +827,46 @@ async function snapshot(transport, described) {
827
827
  }
828
828
  return before;
829
829
  }
830
- var INVERSE = { Inactivate: "Reactivate", Reactivate: "Inactivate", Delete: "Undelete", Undelete: "Delete" };
831
- function reverseOf(described, before, done) {
830
+ var INVERSE = { Inactivate: "Reactivate", QuickClose: "Reactivate", Reactivate: "Inactivate", Delete: "Undelete", Undelete: "Delete" };
831
+ var CASCADING = /* @__PURE__ */ new Set(["Inactivate", "QuickClose"]);
832
+ var CASCADE_DEPTH = 8;
833
+ async function openDescendantsOf(transport, schema, roots) {
834
+ const out = {};
835
+ const rootOf = new Map(roots.map((r) => [momentless(r), momentless(r)]));
836
+ const seen = new Set(rootOf.keys());
837
+ let frontier = [...rootOf.keys()];
838
+ for (let depth = 0; depth < CASCADE_DEPTH && frontier.length; depth += 1) {
839
+ const children = /* @__PURE__ */ new Map();
840
+ for (const [type, list3] of byType(frontier)) {
841
+ await schema.load(type);
842
+ if (!schema.attribute(type, "Children")) continue;
843
+ for (const part of chunks(list3)) {
844
+ const rows = await query(transport, { from: type, select: ["Children"], filter: ["ID=$oids"], with: { $oids: part }, page: { size: part.length, start: 0 } });
845
+ for (const r of rows) {
846
+ const parent = momentless(r._oid);
847
+ for (const c of [].concat(plain(r.Children) ?? [])) {
848
+ const child = momentless(String(c));
849
+ if (seen.has(child)) continue;
850
+ seen.add(child);
851
+ children.set(child, rootOf.get(parent));
852
+ }
853
+ }
854
+ }
855
+ }
856
+ const states = children.size ? await statesOf(transport, [...children.keys()]) : {};
857
+ frontier = [];
858
+ for (const [child, root] of children) {
859
+ if (states[child] === void 0 || isClosed(states[child])) continue;
860
+ rootOf.set(child, root);
861
+ (out[root] ??= []).push(child);
862
+ frontier.push(child);
863
+ }
864
+ }
865
+ return out;
866
+ }
867
+ function reverseOf(described, before, done, context = {}) {
832
868
  const commands = [], notes = [];
869
+ const cannot = (oid, op) => context.canOperate?.(typeOfOid(oid), op) === false;
833
870
  const modified = new Set(done.modified.map(momentless)), operated = new Set(done.operated.map(momentless));
834
871
  for (const u of described.updates) {
835
872
  if (!modified.has(u.oid)) continue;
@@ -854,10 +891,32 @@ function reverseOf(described, before, done) {
854
891
  for (const e of described.executes) {
855
892
  if (!operated.has(e.oid)) continue;
856
893
  const inverse = INVERSE[e.op];
857
- if (inverse) commands.push({ from: e.oid, execute: inverse });
858
- else notes.push(`${e.oid}: ${e.op} has no known inverse (was ${String(before[e.oid]?.AssetState ?? "?")})`);
894
+ if (!inverse) {
895
+ notes.push(`${e.oid}: ${e.op} has no known inverse (was ${String(before[e.oid]?.AssetState ?? "?")})`);
896
+ continue;
897
+ }
898
+ if (cannot(e.oid, inverse)) {
899
+ notes.push(`${e.oid}: ${typeOfOid(e.oid)} has no ${inverse} operation, so its ${e.op} is not reversed`);
900
+ continue;
901
+ }
902
+ commands.push({ from: e.oid, execute: inverse });
903
+ if (!CASCADING.has(e.op)) continue;
904
+ const open = context.openDescendants?.[e.oid] ?? [];
905
+ const closed = context.cascaded ? context.cascaded[e.oid] ?? [] : void 0;
906
+ if (closed === void 0) {
907
+ 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`);
908
+ continue;
909
+ }
910
+ for (const d of closed) {
911
+ if (cannot(d, "Reactivate")) notes.push(`${d}: closed with ${e.oid}, but ${typeOfOid(d)} has no Reactivate operation`);
912
+ else commands.push({ from: d, execute: "Reactivate" });
913
+ }
914
+ if (closed.length) notes.push(`${e.oid}: ${e.op} also closed ${closed.join(", ")}; reopened after it`);
915
+ }
916
+ for (const oid of [...done.created].reverse().map(momentless)) {
917
+ 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)" : ""}`);
918
+ else commands.push({ from: oid, execute: "Delete" });
859
919
  }
860
- for (const oid of [...done.created].reverse()) commands.push({ from: momentless(oid), execute: "Delete" });
861
920
  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`);
862
921
  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");
863
922
  return { commands, notes };
@@ -892,12 +951,14 @@ async function runBatch(transport, schema, commands, options = {}) {
892
951
  const described = describe(commands, projected);
893
952
  const touched = touchedBy(projected);
894
953
  const names = await namesOf(transport, schema, touched, noteFailure);
895
- 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 };
954
+ 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 };
896
955
  const nothing = !described.creates.length && !described.updates.length && !described.executes.length && !described.queries.length && !described.deferred.length;
897
956
  if (nothing) return receipt;
898
957
  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`);
899
958
  receipt.snapshotAt = now();
900
959
  receipt.before = await snapshot(transport, described);
960
+ const closing = described.executes.filter((e) => CASCADING.has(e.op)).map((e) => e.oid);
961
+ if (closing.length) receipt.openDescendants = await openDescendantsOf(transport, schema, closing);
901
962
  if (options.dryRun) return receipt;
902
963
  if (options.confirm && !await options.confirm(receipt)) return receipt;
903
964
  let unstamped = [];
@@ -929,13 +990,29 @@ async function runBatch(transport, schema, commands, options = {}) {
929
990
  const unnamed = { number: null, name: null };
930
991
  receipt.created = done.created.map((oid, i) => ({ alias: described.creates[i]?.alias ?? null, type: described.creates[i]?.type ?? typeOfOid(oid), oid, ...unnamed }));
931
992
  receipt.touched = hit.map((oid) => ({ oid, ...names.get(oid) ?? unnamed }));
932
- receipt.skipped = useGuard ? [.../* @__PURE__ */ new Set([...unstamped, ...touched.filter((oid) => !hit.includes(oid))])] : [];
933
- receipt.reverse = reverseOf(described, receipt.before, done);
993
+ const sentAs = receipt.sentCommands ?? [];
994
+ const oidOfSent = (i) => {
995
+ const body = i === null ? void 0 : sentAs[i] ? bodyOf3(sentAs[i]) : void 0;
996
+ const id = body?.with?.$oid ?? body?.from;
997
+ return typeof id === "string" && id.includes(":") ? momentless(id) : null;
998
+ };
999
+ receipt.failed = [...new Set(done.failures.map((f) => oidOfSent(f.index)).filter((oid) => !!oid && touched.includes(oid) && !hit.includes(oid)))];
1000
+ receipt.skipped = useGuard ? [.../* @__PURE__ */ new Set([...unstamped, ...touched.filter((oid) => !hit.includes(oid) && !receipt.failed.includes(oid))])] : [];
1001
+ for (const type of new Set([...done.created, ...hit].map((oid) => typeOfOid(momentless(oid))))) await schema.load(type).catch(() => null);
1002
+ const canOperate = (type, op) => schema.canOperate(type, op);
1003
+ const operatedNow = new Set(done.operated.map(momentless));
1004
+ const closed = Object.keys(receipt.openDescendants).filter((oid) => operatedNow.has(oid) && receipt.openDescendants[oid].length);
1005
+ receipt.reverse = reverseOf(described, receipt.before, done, { openDescendants: receipt.openDescendants, cascaded: closed.length ? void 0 : {}, canOperate });
934
1006
  try {
1007
+ if (closed.length) {
1008
+ const states = await statesOf(transport, closed.flatMap((oid) => receipt.openDescendants[oid]));
1009
+ receipt.cascaded = Object.fromEntries(closed.map((oid) => [oid, receipt.openDescendants[oid].filter((d) => isClosed(states[d]))]));
1010
+ receipt.reverse = reverseOf(described, receipt.before, done, { openDescendants: receipt.openDescendants, cascaded: receipt.cascaded, canOperate });
1011
+ }
935
1012
  const more = await namesOf(transport, schema, [...done.created, ...hit.filter((oid) => !names.has(oid))], noteFailure);
936
1013
  receipt.created = receipt.created.map((c) => ({ ...c, ...more.get(c.oid) ?? unnamed }));
937
1014
  receipt.touched = receipt.touched.map((t) => names.has(t.oid) ? t : { ...t, ...more.get(t.oid) ?? unnamed });
938
- const all = [...done.created, ...hit];
1015
+ const all = [.../* @__PURE__ */ new Set([...done.created, ...hit, ...Object.values(receipt.cascaded).flat()])];
939
1016
  receipt.after = all.length ? await stampsOf(transport, all) : {};
940
1017
  } catch (error) {
941
1018
  throw new BatchRecordError(error, receipt);
@@ -1099,7 +1176,8 @@ function toAssetType(doc) {
1099
1176
  ...doc.DefaultOrderBy ? { DefaultOrderBy: doc.DefaultOrderBy.tokenref } : {},
1100
1177
  ...doc.DefaultDisplayBy ? { DefaultDisplayBy: doc.DefaultDisplayBy.tokenref } : {},
1101
1178
  ...doc.ShortName ? { ShortName: doc.ShortName.tokenref } : {},
1102
- Attributes: Object.fromEntries(Object.entries(doc.Attributes ?? {}).map(([key, value]) => [key, toAttributeDefinition(value)]))
1179
+ Attributes: Object.fromEntries(Object.entries(doc.Attributes ?? {}).map(([key, value]) => [key, toAttributeDefinition(value)])),
1180
+ ...doc.Operations ? { Operations: Object.keys(doc.Operations).map((token) => token.slice(token.indexOf(".") + 1)) } : {}
1103
1181
  };
1104
1182
  }
1105
1183
  function toMetamodel(doc) {
@@ -1163,6 +1241,15 @@ var Schema = class _Schema {
1163
1241
  attribute(type, name) {
1164
1242
  return attributeOf(this, type, name);
1165
1243
  }
1244
+ /**
1245
+ * Can the type do this operation (Delete, Reactivate, …)? Undefined when
1246
+ * the type is not loaded or its definition carries no operations, so a
1247
+ * caller can tell "no" from "don't know".
1248
+ */
1249
+ canOperate(type, op) {
1250
+ const t = this.assetType(type);
1251
+ return t?.Operations ? t.Operations.includes(op) : void 0;
1252
+ }
1166
1253
  /** The tokens loaded so far. */
1167
1254
  get loaded() {
1168
1255
  return [...this.types.entries()].filter(([, t]) => t !== null).map(([token]) => token);