@atscript/ui 0.1.102 → 0.1.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -948,6 +948,435 @@ function createFieldValidator(prop, opts) {
948
948
  };
949
949
  }
950
950
  //#endregion
951
+ //#region src/form/diff.ts
952
+ /**
953
+ * Diffs a form's `current` data against its `baseline` snapshot, producing both
954
+ * a changed-fields list and an `@atscript/db` patch object.
955
+ *
956
+ * Both `baseline` and `current` are the WRAPPED form-data container
957
+ * (`{ value: domainData }`) so this reuses {@link getByPath}.
958
+ *
959
+ * Revert-aware: a value edited back to its baseline produces no change and no
960
+ * patch entry.
961
+ *
962
+ * Snapshot contract: the result is NOT a deep copy. `$insert` items, `$replace`
963
+ * arrays, scalar leaf values, and `changes[].before/after` all hold live
964
+ * references into `baseline` / `current`. Callers that keep editing the form
965
+ * after building the patch must snapshot first (e.g. build the patch at submit
966
+ * time on a frozen clone). This is the common Vue v-model flow.
967
+ */
968
+ function buildFormDiff(def, baseline, current, opts) {
969
+ const changes = [];
970
+ const patch = {};
971
+ const versionColumn = findVersionColumn(def);
972
+ diffFields(def.fields, "", baseline, current, changes, patch, versionColumn, def.flatMap);
973
+ if ((opts?.cas ?? true) && versionColumn && Object.keys(patch).length > 0) {
974
+ const baselineVersion = getByPath(baseline, versionColumn);
975
+ if (typeof baselineVersion === "number" && Number.isInteger(baselineVersion)) patch.$cas = { [versionColumn]: baselineVersion };
976
+ }
977
+ return {
978
+ isDirty: changes.length > 0,
979
+ changes,
980
+ patch
981
+ };
982
+ }
983
+ /**
984
+ * Diffs a list of sibling fields. `prefix` is the dot-path of the parent
985
+ * context relative to the form root (used only for the change list `path`);
986
+ * patch entries are written into the local `patch` object so callers can place
987
+ * the whole sub-object as a nested partial.
988
+ *
989
+ * `versionColumn` is the top-level `@db.column.version` field name (or
990
+ * undefined). When set, the matching top-level field is skipped entirely — it
991
+ * is server-managed and may only be round-tripped via `$cas`.
992
+ *
993
+ * `inlineFlatMap` is the form's `flatMap`, supplied only at the top-level walk.
994
+ * It lets {@link diffScalarField} read `@db.patch.strategy` off the object
995
+ * ancestors of an INLINED leaf (a dotted-path leaf with no `FormObjectFieldDef`
996
+ * node — `createFormDef` dissolves unlabelled objects into dot-paths). At a
997
+ * default (replace) ancestor the whole sub-object must be emitted. It is
998
+ * intentionally NOT propagated into `diffObjectField` recursion, where the
999
+ * strategy decision is already made per structured object.
1000
+ */
1001
+ function diffFields(fields, prefix, baseline, current, changes, patch, versionColumn, inlineFlatMap) {
1002
+ for (const field of fields) {
1003
+ if (field.path === "" && fields.length === 1) {
1004
+ diffLeafRoot(field, baseline, current, changes, patch);
1005
+ continue;
1006
+ }
1007
+ if (field.phantom) continue;
1008
+ if (versionColumn !== void 0 && !prefix && field.path === versionColumn) continue;
1009
+ const fullPath = prefix ? `${prefix}.${field.path}` : field.path;
1010
+ if (isArrayField(field)) {
1011
+ diffArrayField(field, fullPath, baseline, current, changes, patch);
1012
+ continue;
1013
+ }
1014
+ if (isObjectField(field)) {
1015
+ diffObjectField(field, fullPath, baseline, current, changes, patch);
1016
+ continue;
1017
+ }
1018
+ diffScalarField(field, fullPath, baseline, current, changes, patch, inlineFlatMap);
1019
+ }
1020
+ }
1021
+ /** Single-leaf root form (non-object root). Whole value is the patch. */
1022
+ function diffLeafRoot(field, baseline, current, changes, patch) {
1023
+ const before = getByPath(baseline, "");
1024
+ const after = getByPath(current, "");
1025
+ if (deepEqual(before, after)) return;
1026
+ if (isArrayField(field)) {
1027
+ const arrayPatch = diffArray(field, before, after);
1028
+ if (arrayPatch === void 0) return;
1029
+ changes.push({
1030
+ path: "",
1031
+ kind: "array",
1032
+ before,
1033
+ after
1034
+ });
1035
+ patch.value = arrayPatch;
1036
+ } else {
1037
+ changes.push({
1038
+ path: "",
1039
+ kind: "set",
1040
+ before,
1041
+ after
1042
+ });
1043
+ patch.value = after === void 0 ? null : after;
1044
+ }
1045
+ }
1046
+ /** Scalar / union / tuple / ref field — whole-value compare; clear → null. */
1047
+ function diffScalarField(field, fullPath, baseline, current, changes, patch, inlineFlatMap) {
1048
+ const before = getByPath(baseline, fullPath);
1049
+ const after = getByPath(current, fullPath);
1050
+ if (deepEqual(before, after)) return;
1051
+ changes.push({
1052
+ path: fullPath,
1053
+ kind: "set",
1054
+ before,
1055
+ after
1056
+ });
1057
+ if (inlineFlatMap && field.path.includes(".")) {
1058
+ const cutoff = inlinedReplaceCutoff(field.path, inlineFlatMap);
1059
+ if (cutoff !== void 0) {
1060
+ const prefixLen = fullPath.length - field.path.length;
1061
+ const sub = getByPath(current, prefixLen > 0 ? fullPath.slice(0, prefixLen) + cutoff : cutoff);
1062
+ setPatchLeaf(patch, cutoff, sub === void 0 ? null : sub);
1063
+ return;
1064
+ }
1065
+ }
1066
+ setPatchLeaf(patch, field.path, after === void 0 ? null : after);
1067
+ }
1068
+ /**
1069
+ * For a dotted INLINED leaf path, returns the path of the shallowest object
1070
+ * ancestor whose `@db.patch.strategy` is the default (`replace`), or undefined
1071
+ * when every ancestor is `merge` (then the leaf partial is correct). At a
1072
+ * replace ancestor the whole sub-object must be present in the patch.
1073
+ *
1074
+ * Walks ancestor segments (`a`, `a.b`, … but not the leaf itself); the first
1075
+ * one that is an object AND not merge is the cutoff. merge does NOT propagate,
1076
+ * so a default-replace level below a merge level still cuts off there.
1077
+ */
1078
+ function inlinedReplaceCutoff(leafPath, flatMap) {
1079
+ const segs = leafPath.split(".");
1080
+ let acc = "";
1081
+ for (let i = 0; i < segs.length - 1; i++) {
1082
+ acc = acc ? `${acc}.${segs[i]}` : segs[i];
1083
+ const prop = flatMap.get(acc);
1084
+ if (!prop || prop.type.kind !== "object") continue;
1085
+ if (getFieldMeta(prop, "db.patch.strategy") !== "merge") return acc;
1086
+ }
1087
+ }
1088
+ /**
1089
+ * Inlined-or-structured object field — recurse, emit a nested partial OR the
1090
+ * whole sub-object depending on the field's `@db.patch.strategy`.
1091
+ *
1092
+ * atscript-db's DEFAULT nested-object patch strategy is `replace` (strict —
1093
+ * every required child must be present, else 400; omitted optionals are
1094
+ * null-filled). A changed-leaves-only partial is a valid patch ONLY when the
1095
+ * object field carries `@db.patch.strategy 'merge'`. For the default (replace)
1096
+ * case we therefore emit the WHOLE current sub-object so the validator passes
1097
+ * and no optional leaf is silently nulled. `merge` does NOT propagate, so a
1098
+ * descendant object without its own `merge` again emits its full sub-object
1099
+ * (handled by recursion — `diffFields` re-enters this function per child).
1100
+ *
1101
+ * Wholesale-clear: if the sub-object was a defined object in `baseline` but is
1102
+ * now undefined/null, emit `field: null` (object removed) instead of a partial
1103
+ * of nulled leaves.
1104
+ */
1105
+ function diffObjectField(field, fullPath, baseline, current, changes, patch) {
1106
+ const beforeObj = getByPath(baseline, fullPath);
1107
+ const afterObj = getByPath(current, fullPath);
1108
+ const nested = {};
1109
+ diffFields(field.objectDef.fields, fullPath, baseline, current, changes, nested, void 0, field.objectDef.flatMap);
1110
+ if ((afterObj === void 0 || afterObj === null) && isPlainObject(beforeObj)) {
1111
+ setPatchLeaf(patch, field.path, null);
1112
+ return;
1113
+ }
1114
+ if (Object.keys(nested).length === 0) return;
1115
+ if (getFieldMeta(field.prop, "db.patch.strategy") === "merge") setPatchLeaf(patch, field.path, nested);
1116
+ else setPatchLeaf(patch, field.path, afterObj === void 0 ? null : afterObj);
1117
+ }
1118
+ /** Array field — keyed → $update/$insert/$remove; unkeyed → $replace. */
1119
+ function diffArrayField(field, fullPath, baseline, current, changes, patch) {
1120
+ const before = getByPath(baseline, fullPath);
1121
+ const after = getByPath(current, fullPath);
1122
+ if (deepEqual(before, after)) return;
1123
+ const arrayPatch = diffArray(field, before, after);
1124
+ if (arrayPatch === void 0) return;
1125
+ changes.push({
1126
+ path: fullPath,
1127
+ kind: "array",
1128
+ before,
1129
+ after
1130
+ });
1131
+ setPatchLeaf(patch, field.path, arrayPatch);
1132
+ }
1133
+ /**
1134
+ * Produces a `TArrayPatch` value for one array field, or `undefined` when no
1135
+ * real op results (the caller then skips the field entirely).
1136
+ *
1137
+ * - Keyed arrays (item object has `@expect.array.key`): emit `$update`
1138
+ * (key + changed leaves), `$insert` (wholly-new items, whole), `$remove`
1139
+ * (key only). Reorder-only (same key membership, same content, different
1140
+ * order) → `$replace` (key-ops can't express a pure reorder).
1141
+ * Ambiguous keys (duplicate or missing key values) → `$replace` (the only
1142
+ * faithful op when key identity is unreliable).
1143
+ * - Unkeyed object arrays / primitive arrays: `$replace` with the whole array.
1144
+ * Primitive arrays with `@expect.array.uniqueItems` use by-value
1145
+ * `$insert` / `$remove` (set semantics).
1146
+ *
1147
+ * Deliberate `$insert`-not-`$upsert`: wholly-new keyed items use `$insert`
1148
+ * (pure append) rather than `$upsert`. This is safe because `$insert` is only
1149
+ * ever used for keys ABSENT from baseline; existing keys go through `$update`.
1150
+ * `$upsert` would dedupe-by-key, which we don't need given that invariant.
1151
+ */
1152
+ function diffArray(field, before, after) {
1153
+ const beforeArr = Array.isArray(before) ? before : [];
1154
+ const afterArr = Array.isArray(after) ? after : [];
1155
+ const keyProps = getArrayKeyProps(field.itemType);
1156
+ if (keyProps.length > 0) return diffKeyedArray(beforeArr, afterArr, keyProps);
1157
+ if (getFieldMeta(field.prop, "expect.array.uniqueItems") !== void 0 && isPrimitiveItem(field)) {
1158
+ const setPatch = diffUniqueArray(beforeArr, afterArr);
1159
+ if (setPatch) return setPatch;
1160
+ }
1161
+ return { $replace: afterArr };
1162
+ }
1163
+ /**
1164
+ * Keyed array diff. Reorder-only (same membership) falls back to $replace.
1165
+ *
1166
+ * Returns `undefined` when no $update/$insert/$remove op is produced (so the
1167
+ * caller skips the field rather than emitting a malformed empty `{}`).
1168
+ *
1169
+ * Ambiguity fallback: if either side has DUPLICATE key buckets, or ANY element
1170
+ * is missing all of its key values, key identity is unreliable — fall back to
1171
+ * `{ $replace: after }`, the only faithful op (last-write-wins collapse would
1172
+ * silently drop items, and a key-less `$update` is unmatchable by the DB).
1173
+ */
1174
+ function diffKeyedArray(before, after, keyProps) {
1175
+ if (hasKeylessItem(before, keyProps) || hasKeylessItem(after, keyProps)) return { $replace: after };
1176
+ const beforeByKey = /* @__PURE__ */ new Map();
1177
+ for (const el of before) if (isPlainObject(el)) beforeByKey.set(keyOf(el, keyProps), el);
1178
+ const afterByKey = /* @__PURE__ */ new Map();
1179
+ for (const el of after) if (isPlainObject(el)) afterByKey.set(keyOf(el, keyProps), el);
1180
+ if (beforeByKey.size !== before.length || afterByKey.size !== after.length) return { $replace: after };
1181
+ if (beforeByKey.size === afterByKey.size) {
1182
+ let sameMembershipAndContent = true;
1183
+ for (const [k, el] of afterByKey) {
1184
+ const prev = beforeByKey.get(k);
1185
+ if (prev === void 0 || !deepEqual(prev, el)) {
1186
+ sameMembershipAndContent = false;
1187
+ break;
1188
+ }
1189
+ }
1190
+ if (sameMembershipAndContent) return { $replace: after };
1191
+ }
1192
+ const $insert = [];
1193
+ const $update = [];
1194
+ const $remove = [];
1195
+ for (const [k, el] of afterByKey) {
1196
+ const prev = beforeByKey.get(k);
1197
+ if (prev === void 0) {
1198
+ $insert.push(el);
1199
+ continue;
1200
+ }
1201
+ if (!deepEqual(prev, el)) {
1202
+ const partial = buildKeyedUpdate(prev, el, keyProps);
1203
+ if (partial) $update.push(partial);
1204
+ }
1205
+ }
1206
+ for (const [k, el] of beforeByKey) if (!afterByKey.has(k)) $remove.push(pickKeys(el, keyProps));
1207
+ return arrayOps({
1208
+ $update,
1209
+ $insert,
1210
+ $remove
1211
+ });
1212
+ }
1213
+ /** Builds a `$update` partial: key fields + changed leaves only. */
1214
+ function buildKeyedUpdate(prev, next, keyProps) {
1215
+ const partial = {};
1216
+ for (const k of keyProps) partial[k] = next[k];
1217
+ let changed = false;
1218
+ const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
1219
+ for (const k of allKeys) {
1220
+ if (keyProps.includes(k)) continue;
1221
+ const a = prev[k];
1222
+ const b = next[k];
1223
+ if (!deepEqual(a, b)) {
1224
+ partial[k] = b === void 0 ? null : b;
1225
+ changed = true;
1226
+ }
1227
+ }
1228
+ return changed ? partial : void 0;
1229
+ }
1230
+ /** Primitive uniqueItems set diff. Returns undefined if neither side differs. */
1231
+ function diffUniqueArray(before, after) {
1232
+ const beforeSet = new Set(before.map((v) => stableKey(v)));
1233
+ const afterSet = new Set(after.map((v) => stableKey(v)));
1234
+ const $insert = [];
1235
+ const $remove = [];
1236
+ const seenInsert = /* @__PURE__ */ new Set();
1237
+ const seenRemove = /* @__PURE__ */ new Set();
1238
+ for (const v of after) {
1239
+ const k = stableKey(v);
1240
+ if (!beforeSet.has(k) && !seenInsert.has(k)) {
1241
+ $insert.push(v);
1242
+ seenInsert.add(k);
1243
+ }
1244
+ }
1245
+ for (const v of before) {
1246
+ const k = stableKey(v);
1247
+ if (!afterSet.has(k) && !seenRemove.has(k)) {
1248
+ $remove.push(v);
1249
+ seenRemove.add(k);
1250
+ }
1251
+ }
1252
+ return arrayOps({
1253
+ $insert,
1254
+ $remove
1255
+ });
1256
+ }
1257
+ /**
1258
+ * Assembles a `TArrayPatch` from named op-arrays, dropping empty ones. Returns
1259
+ * `undefined` when no op carries any item, so the caller skips the field rather
1260
+ * than emitting a malformed empty `{}`.
1261
+ */
1262
+ function arrayOps(ops) {
1263
+ const result = {};
1264
+ for (const k in ops) if (ops[k].length > 0) result[k] = ops[k];
1265
+ return Object.keys(result).length > 0 ? result : void 0;
1266
+ }
1267
+ /** Reads `@expect.array.key` props from an array item's object type. */
1268
+ function getArrayKeyProps(itemType) {
1269
+ if (itemType.type.kind !== "object") return [];
1270
+ const props = itemType.type.props;
1271
+ const keys = [];
1272
+ for (const [name, prop] of props.entries()) if (getFieldMeta(prop, "expect.array.key") !== void 0) keys.push(name);
1273
+ return keys;
1274
+ }
1275
+ /** True when the array's item type is a primitive (designType, kind === ''). */
1276
+ function isPrimitiveItem(field) {
1277
+ return field.itemType.type.kind === "";
1278
+ }
1279
+ /** Finds the form's `@db.column.version` column name, if any (top-level only). */
1280
+ function findVersionColumn(def) {
1281
+ for (const [path, prop] of def.flatMap.entries()) {
1282
+ if (!path || path.includes(".")) continue;
1283
+ if (getFieldMeta(prop, "db.column.version") !== void 0) return path;
1284
+ }
1285
+ }
1286
+ /** True when any element lacks ALL of its key values (un-keyable identity). */
1287
+ function hasKeylessItem(arr, keyProps) {
1288
+ for (const el of arr) {
1289
+ if (!isPlainObject(el)) return true;
1290
+ let hasAnyKey = false;
1291
+ for (const k of keyProps) {
1292
+ const v = el[k];
1293
+ if (v !== void 0 && v !== null) {
1294
+ hasAnyKey = true;
1295
+ break;
1296
+ }
1297
+ }
1298
+ if (!hasAnyKey) return true;
1299
+ }
1300
+ return false;
1301
+ }
1302
+ /** Composite key string for a keyed-array element. */
1303
+ function keyOf(el, keyProps) {
1304
+ if (keyProps.length === 1) return stableKey(el[keyProps[0]]);
1305
+ return keyProps.map((k) => stableKey(el[k])).join(" ");
1306
+ }
1307
+ /** Picks only the key fields from an element (for $remove). */
1308
+ function pickKeys(el, keyProps) {
1309
+ const out = {};
1310
+ for (const k of keyProps) out[k] = el[k];
1311
+ return out;
1312
+ }
1313
+ /** Writes a value into a (possibly dotted) leaf path on a local patch object. */
1314
+ function setPatchLeaf(patch, path, value) {
1315
+ if (!path.includes(".")) {
1316
+ patch[path] = value;
1317
+ return;
1318
+ }
1319
+ const keys = path.split(".");
1320
+ const last = keys.pop();
1321
+ let cur = patch;
1322
+ for (const k of keys) {
1323
+ let next = cur[k];
1324
+ if (next === void 0 || next === null || typeof next !== "object") {
1325
+ next = {};
1326
+ cur[k] = next;
1327
+ }
1328
+ cur = next;
1329
+ }
1330
+ cur[last] = value;
1331
+ }
1332
+ function isPlainObject(v) {
1333
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1334
+ }
1335
+ /**
1336
+ * Stable string key for primitives / values (used for set membership).
1337
+ *
1338
+ * Key equality is TYPE-STRICT: a number `1` and a string `'1'` produce
1339
+ * distinct keys, so a keyed-array item whose key changes JS representation
1340
+ * between baseline and current is treated as a remove + insert. Callers that
1341
+ * round-trip keys with loose typing should normalise the key type first.
1342
+ */
1343
+ function stableKey(v) {
1344
+ if (typeof v === "string") return `s:${v}`;
1345
+ if (typeof v === "number" || typeof v === "boolean") return `p:${String(v)}`;
1346
+ if (v === null) return "null";
1347
+ if (v === void 0) return "undef";
1348
+ return `j:${JSON.stringify(v)}`;
1349
+ }
1350
+ /**
1351
+ * Structural deep equality (order-sensitive for arrays). `NaN` equals `NaN`
1352
+ * (revert-aware for NaN scalars) while `0` / `-0` stay equal (matches DB
1353
+ * intent — `===` treats them equal, only NaN is special-cased).
1354
+ */
1355
+ function deepEqual(a, b) {
1356
+ if (a === b) return true;
1357
+ if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
1358
+ if (a === null || b === null || a === void 0 || b === void 0) return false;
1359
+ if (typeof a !== "object" || typeof b !== "object") return false;
1360
+ const aIsArr = Array.isArray(a);
1361
+ const bIsArr = Array.isArray(b);
1362
+ if (aIsArr !== bIsArr) return false;
1363
+ if (aIsArr && bIsArr) {
1364
+ if (a.length !== b.length) return false;
1365
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
1366
+ return true;
1367
+ }
1368
+ const ao = a;
1369
+ const bo = b;
1370
+ const aKeys = Object.keys(ao);
1371
+ const bKeys = Object.keys(bo);
1372
+ if (aKeys.length !== bKeys.length) return false;
1373
+ for (const k of aKeys) {
1374
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
1375
+ if (!deepEqual(ao[k], bo[k])) return false;
1376
+ }
1377
+ return true;
1378
+ }
1379
+ //#endregion
951
1380
  //#region src/form/error-utils.ts
952
1381
  /**
953
1382
  * Framework-agnostic helpers for working with form-error maps keyed by
@@ -1636,6 +2065,7 @@ exports.ValueHelpClient = ValueHelpClient;
1636
2065
  exports.WF_ACTION_WITH_DATA = WF_ACTION_WITH_DATA;
1637
2066
  exports.asArray = asArray;
1638
2067
  exports.buildDescendantErrorCounts = buildDescendantErrorCounts;
2068
+ exports.buildFormDiff = buildFormDiff;
1639
2069
  exports.buildGridClasses = buildGridClasses;
1640
2070
  exports.buildUnionVariants = buildUnionVariants;
1641
2071
  exports.createFieldValidator = createFieldValidator;
package/dist/index.d.cts CHANGED
@@ -484,6 +484,73 @@ declare function createFieldValidator(prop: TAtscriptAnnotatedType, opts?: TFiel
484
484
  context: unknown;
485
485
  }) => true | string;
486
486
  //#endregion
487
+ //#region src/form/diff.d.ts
488
+ /**
489
+ * One field that differs between baseline and current.
490
+ *
491
+ * - `kind: 'set'` — scalar / object / union / tuple field whose value changed
492
+ * (including a clear-to-`null`). `before` / `after` are the whole values at
493
+ * `path`.
494
+ * - `kind: 'array'` — array field whose membership or item content changed.
495
+ * `before` / `after` are the whole arrays.
496
+ *
497
+ * NOTE: `before` / `after` hold live references into the supplied `baseline` /
498
+ * `current` containers — see {@link buildFormDiff} for the snapshot contract.
499
+ */
500
+ interface FormFieldChange {
501
+ /** Dot-separated path relative to the form root (matches FormFieldDef.path). */
502
+ path: string;
503
+ kind: "set" | "array";
504
+ before: unknown;
505
+ after: unknown;
506
+ }
507
+ /** Options for {@link buildFormDiff}. */
508
+ interface FormDiffOptions {
509
+ /**
510
+ * Optimistic-concurrency control. When `true` (default), a top-level
511
+ * `$cas: { [versionColumn]: baselineVersion }` sibling is auto-included in
512
+ * the patch whenever the form has a `@db.column.version` column AND the
513
+ * patch is non-empty AND a baseline version value exists. `false` suppresses
514
+ * it entirely.
515
+ *
516
+ * Independent of `$cas`, the `@db.column.version` column is ALWAYS excluded
517
+ * from the SET diff: it is server-managed, and a direct write to it is
518
+ * rejected by `@atscript/db` (`DbError('VERSION_COLUMN_WRITE')`). It is only
519
+ * ever round-tripped through `$cas`.
520
+ */
521
+ cas?: boolean;
522
+ }
523
+ /** Result of {@link buildFormDiff}. */
524
+ interface FormDiffResult {
525
+ /** True when at least one field changed (revert-aware). */
526
+ isDirty: boolean;
527
+ /** Per-field changes (revert-aware — reverted fields are absent). */
528
+ changes: FormFieldChange[];
529
+ /**
530
+ * `@atscript/db` patch object — flat, keyed by field name. Empty `{}` when
531
+ * nothing changed. Carries a top-level `$cas` sibling when `opts.cas` is on
532
+ * and a version column exists.
533
+ */
534
+ patch: Record<string, unknown>;
535
+ }
536
+ /**
537
+ * Diffs a form's `current` data against its `baseline` snapshot, producing both
538
+ * a changed-fields list and an `@atscript/db` patch object.
539
+ *
540
+ * Both `baseline` and `current` are the WRAPPED form-data container
541
+ * (`{ value: domainData }`) so this reuses {@link getByPath}.
542
+ *
543
+ * Revert-aware: a value edited back to its baseline produces no change and no
544
+ * patch entry.
545
+ *
546
+ * Snapshot contract: the result is NOT a deep copy. `$insert` items, `$replace`
547
+ * arrays, scalar leaf values, and `changes[].before/after` all hold live
548
+ * references into `baseline` / `current`. Callers that keep editing the form
549
+ * after building the patch must snapshot first (e.g. build the patch at submit
550
+ * time on a frozen clone). This is the common Vue v-model flow.
551
+ */
552
+ declare function buildFormDiff(def: FormDef, baseline: Record<string, unknown>, current: Record<string, unknown>, opts?: FormDiffOptions): FormDiffResult;
553
+ //#endregion
487
554
  //#region src/form/error-utils.d.ts
488
555
  /**
489
556
  * Framework-agnostic helpers for working with form-error maps keyed by
@@ -912,4 +979,4 @@ declare function getFilterableColumns(def: TableDef): ColumnDef[];
912
979
  /** Find a column by path. */
913
980
  declare function getColumn(def: TableDef, path: string): ColumnDef | undefined;
914
981
  //#endregion
915
- export { type ClientFactory, type ColumnDef, type CurrencyDisplay, DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_COLUMN_PRECISION, DB_HTTP_PATH, DB_REL_FK, DB_UNIT, DB_UNIT_REF, DEFAULT_COL_SPAN, DEFAULT_ROW_SPAN, type DecimalParts, EXPECT_MAX_LENGTH, type FieldMeta, type FieldResolver, type FormActionInfo, type FormArrayFieldDef, type FormDef, type FormFieldDef, type FormObjectFieldDef, type FormTupleFieldDef, type FormUnionFieldDef, type FormUnionVariant, type FormatDecimalOptions, type GridSpanArgs, type GridSpec, META_DEFAULT, META_DESCRIPTION, META_ID, META_LABEL, META_READONLY, META_REQUIRED, META_SENSITIVE, type MeasurementInfo, type MetaCacheEntry, type MetaResponse, type PaginationControl, type RelationInfo, type ResolvedValueHelp, type SearchIndexInfo, type SortControl, StaticFieldResolver, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TFieldValidatorOptions, type TFormAction, type TFormEntryOptions, type TFormValidatorCallOptions, type TFormValueResolver, type TResolveOptions, type TableActionsModel, type TableDef, type TableQueryState, UI_DICT_ATTR, UI_DICT_DESCR, UI_DICT_FILTERABLE, UI_DICT_LABEL, UI_DICT_SEARCHABLE, UI_DICT_SORTABLE, UI_FORM_ACTION, UI_FORM_ATTR, UI_FORM_AUTOCOMPLETE, UI_FORM_CLASSES, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_CLASSES, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_HINT, UI_FORM_FN_LABEL, UI_FORM_FN_OPTIONS, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_FN_STYLES, UI_FORM_FN_SUBMIT_DISABLED, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_TITLE, UI_FORM_FN_VALUE, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_HIDDEN, UI_FORM_HINT, UI_FORM_LABEL_SINGULAR, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_PLACEHOLDER, UI_FORM_PREFIX, UI_FORM_PREFIX_ICON, UI_FORM_PREFIX_REF, UI_FORM_STYLES, UI_FORM_SUBMIT_TEXT, UI_FORM_SUFFIX, UI_FORM_SUFFIX_ICON, UI_FORM_SUFFIX_REF, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_COMPONENT, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_PREFIX, UI_TABLE_FN_STYLES, UI_TABLE_HIDDEN, UI_TABLE_ORDER, UI_TABLE_SELECT_WITH, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_WIDTH, UI_TYPE, ValueHelpClient, type ValueHelpInfo, type ValueHelpResult, type ValueHelpSearchOptions, WF_ACTION_WITH_DATA, asArray, buildDescendantErrorCounts, buildGridClasses, buildUnionVariants, createFieldValidator, createFormData, createFormDef, createFormValueResolver, createTableDef, defaultResolver, detectUnionVariant, enforceScale, extractLiteralOptions, extractMeasurement, extractValueHelp, formatDecimalForDisplay, getByPath, getColumn, getCurrencyDecimals, getCurrencyDisplayParts, getDecimalSeparator, getDeclaredFormActions, getDefaultClientFactory, getDefaultValidatorPlugins, getFieldMeta, getFilterableColumns, getFormValidator, getMetaEntry, getResolver, getSortableColumns, getThousandsSeparator, getVisibleColumns, groupInteger, hasComputedAnnotations, isArrayField, isObjectField, isPureLiteralUnion, isTupleField, isUnionField, iteratePathAncestors, joinDecimalString, mergeErrorMaps, optKey, optLabel, parseColSpan, parseDecimalInput, parseRowSpan, parseStaticAttrs, parseStaticOptions, resetDefaultClientFactory, resetMetaCache, resetValueHelpCache, resolveAttrs, resolveFieldProp, resolveFormProp, resolveGridSpec, resolveOptions, resolveSingularLabel, resolveStatic, resolveValueHelp, setByPath, setDefaultClientFactory, setDefaultValidatorPlugins, setResolver, splitDecimalString, str, valueHelpDictPaths };
982
+ export { type ClientFactory, type ColumnDef, type CurrencyDisplay, DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_COLUMN_PRECISION, DB_HTTP_PATH, DB_REL_FK, DB_UNIT, DB_UNIT_REF, DEFAULT_COL_SPAN, DEFAULT_ROW_SPAN, type DecimalParts, EXPECT_MAX_LENGTH, type FieldMeta, type FieldResolver, type FormActionInfo, type FormArrayFieldDef, type FormDef, type FormDiffOptions, type FormDiffResult, type FormFieldChange, type FormFieldDef, type FormObjectFieldDef, type FormTupleFieldDef, type FormUnionFieldDef, type FormUnionVariant, type FormatDecimalOptions, type GridSpanArgs, type GridSpec, META_DEFAULT, META_DESCRIPTION, META_ID, META_LABEL, META_READONLY, META_REQUIRED, META_SENSITIVE, type MeasurementInfo, type MetaCacheEntry, type MetaResponse, type PaginationControl, type RelationInfo, type ResolvedValueHelp, type SearchIndexInfo, type SortControl, StaticFieldResolver, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TFieldValidatorOptions, type TFormAction, type TFormEntryOptions, type TFormValidatorCallOptions, type TFormValueResolver, type TResolveOptions, type TableActionsModel, type TableDef, type TableQueryState, UI_DICT_ATTR, UI_DICT_DESCR, UI_DICT_FILTERABLE, UI_DICT_LABEL, UI_DICT_SEARCHABLE, UI_DICT_SORTABLE, UI_FORM_ACTION, UI_FORM_ATTR, UI_FORM_AUTOCOMPLETE, UI_FORM_CLASSES, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_CLASSES, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_HINT, UI_FORM_FN_LABEL, UI_FORM_FN_OPTIONS, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_FN_STYLES, UI_FORM_FN_SUBMIT_DISABLED, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_TITLE, UI_FORM_FN_VALUE, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_HIDDEN, UI_FORM_HINT, UI_FORM_LABEL_SINGULAR, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_PLACEHOLDER, UI_FORM_PREFIX, UI_FORM_PREFIX_ICON, UI_FORM_PREFIX_REF, UI_FORM_STYLES, UI_FORM_SUBMIT_TEXT, UI_FORM_SUFFIX, UI_FORM_SUFFIX_ICON, UI_FORM_SUFFIX_REF, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_COMPONENT, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_PREFIX, UI_TABLE_FN_STYLES, UI_TABLE_HIDDEN, UI_TABLE_ORDER, UI_TABLE_SELECT_WITH, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_WIDTH, UI_TYPE, ValueHelpClient, type ValueHelpInfo, type ValueHelpResult, type ValueHelpSearchOptions, WF_ACTION_WITH_DATA, asArray, buildDescendantErrorCounts, buildFormDiff, buildGridClasses, buildUnionVariants, createFieldValidator, createFormData, createFormDef, createFormValueResolver, createTableDef, defaultResolver, detectUnionVariant, enforceScale, extractLiteralOptions, extractMeasurement, extractValueHelp, formatDecimalForDisplay, getByPath, getColumn, getCurrencyDecimals, getCurrencyDisplayParts, getDecimalSeparator, getDeclaredFormActions, getDefaultClientFactory, getDefaultValidatorPlugins, getFieldMeta, getFilterableColumns, getFormValidator, getMetaEntry, getResolver, getSortableColumns, getThousandsSeparator, getVisibleColumns, groupInteger, hasComputedAnnotations, isArrayField, isObjectField, isPureLiteralUnion, isTupleField, isUnionField, iteratePathAncestors, joinDecimalString, mergeErrorMaps, optKey, optLabel, parseColSpan, parseDecimalInput, parseRowSpan, parseStaticAttrs, parseStaticOptions, resetDefaultClientFactory, resetMetaCache, resetValueHelpCache, resolveAttrs, resolveFieldProp, resolveFormProp, resolveGridSpec, resolveOptions, resolveSingularLabel, resolveStatic, resolveValueHelp, setByPath, setDefaultClientFactory, setDefaultValidatorPlugins, setResolver, splitDecimalString, str, valueHelpDictPaths };
package/dist/index.d.mts CHANGED
@@ -484,6 +484,73 @@ declare function createFieldValidator(prop: TAtscriptAnnotatedType, opts?: TFiel
484
484
  context: unknown;
485
485
  }) => true | string;
486
486
  //#endregion
487
+ //#region src/form/diff.d.ts
488
+ /**
489
+ * One field that differs between baseline and current.
490
+ *
491
+ * - `kind: 'set'` — scalar / object / union / tuple field whose value changed
492
+ * (including a clear-to-`null`). `before` / `after` are the whole values at
493
+ * `path`.
494
+ * - `kind: 'array'` — array field whose membership or item content changed.
495
+ * `before` / `after` are the whole arrays.
496
+ *
497
+ * NOTE: `before` / `after` hold live references into the supplied `baseline` /
498
+ * `current` containers — see {@link buildFormDiff} for the snapshot contract.
499
+ */
500
+ interface FormFieldChange {
501
+ /** Dot-separated path relative to the form root (matches FormFieldDef.path). */
502
+ path: string;
503
+ kind: "set" | "array";
504
+ before: unknown;
505
+ after: unknown;
506
+ }
507
+ /** Options for {@link buildFormDiff}. */
508
+ interface FormDiffOptions {
509
+ /**
510
+ * Optimistic-concurrency control. When `true` (default), a top-level
511
+ * `$cas: { [versionColumn]: baselineVersion }` sibling is auto-included in
512
+ * the patch whenever the form has a `@db.column.version` column AND the
513
+ * patch is non-empty AND a baseline version value exists. `false` suppresses
514
+ * it entirely.
515
+ *
516
+ * Independent of `$cas`, the `@db.column.version` column is ALWAYS excluded
517
+ * from the SET diff: it is server-managed, and a direct write to it is
518
+ * rejected by `@atscript/db` (`DbError('VERSION_COLUMN_WRITE')`). It is only
519
+ * ever round-tripped through `$cas`.
520
+ */
521
+ cas?: boolean;
522
+ }
523
+ /** Result of {@link buildFormDiff}. */
524
+ interface FormDiffResult {
525
+ /** True when at least one field changed (revert-aware). */
526
+ isDirty: boolean;
527
+ /** Per-field changes (revert-aware — reverted fields are absent). */
528
+ changes: FormFieldChange[];
529
+ /**
530
+ * `@atscript/db` patch object — flat, keyed by field name. Empty `{}` when
531
+ * nothing changed. Carries a top-level `$cas` sibling when `opts.cas` is on
532
+ * and a version column exists.
533
+ */
534
+ patch: Record<string, unknown>;
535
+ }
536
+ /**
537
+ * Diffs a form's `current` data against its `baseline` snapshot, producing both
538
+ * a changed-fields list and an `@atscript/db` patch object.
539
+ *
540
+ * Both `baseline` and `current` are the WRAPPED form-data container
541
+ * (`{ value: domainData }`) so this reuses {@link getByPath}.
542
+ *
543
+ * Revert-aware: a value edited back to its baseline produces no change and no
544
+ * patch entry.
545
+ *
546
+ * Snapshot contract: the result is NOT a deep copy. `$insert` items, `$replace`
547
+ * arrays, scalar leaf values, and `changes[].before/after` all hold live
548
+ * references into `baseline` / `current`. Callers that keep editing the form
549
+ * after building the patch must snapshot first (e.g. build the patch at submit
550
+ * time on a frozen clone). This is the common Vue v-model flow.
551
+ */
552
+ declare function buildFormDiff(def: FormDef, baseline: Record<string, unknown>, current: Record<string, unknown>, opts?: FormDiffOptions): FormDiffResult;
553
+ //#endregion
487
554
  //#region src/form/error-utils.d.ts
488
555
  /**
489
556
  * Framework-agnostic helpers for working with form-error maps keyed by
@@ -912,4 +979,4 @@ declare function getFilterableColumns(def: TableDef): ColumnDef[];
912
979
  /** Find a column by path. */
913
980
  declare function getColumn(def: TableDef, path: string): ColumnDef | undefined;
914
981
  //#endregion
915
- export { type ClientFactory, type ColumnDef, type CurrencyDisplay, DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_COLUMN_PRECISION, DB_HTTP_PATH, DB_REL_FK, DB_UNIT, DB_UNIT_REF, DEFAULT_COL_SPAN, DEFAULT_ROW_SPAN, type DecimalParts, EXPECT_MAX_LENGTH, type FieldMeta, type FieldResolver, type FormActionInfo, type FormArrayFieldDef, type FormDef, type FormFieldDef, type FormObjectFieldDef, type FormTupleFieldDef, type FormUnionFieldDef, type FormUnionVariant, type FormatDecimalOptions, type GridSpanArgs, type GridSpec, META_DEFAULT, META_DESCRIPTION, META_ID, META_LABEL, META_READONLY, META_REQUIRED, META_SENSITIVE, type MeasurementInfo, type MetaCacheEntry, type MetaResponse, type PaginationControl, type RelationInfo, type ResolvedValueHelp, type SearchIndexInfo, type SortControl, StaticFieldResolver, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TFieldValidatorOptions, type TFormAction, type TFormEntryOptions, type TFormValidatorCallOptions, type TFormValueResolver, type TResolveOptions, type TableActionsModel, type TableDef, type TableQueryState, UI_DICT_ATTR, UI_DICT_DESCR, UI_DICT_FILTERABLE, UI_DICT_LABEL, UI_DICT_SEARCHABLE, UI_DICT_SORTABLE, UI_FORM_ACTION, UI_FORM_ATTR, UI_FORM_AUTOCOMPLETE, UI_FORM_CLASSES, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_CLASSES, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_HINT, UI_FORM_FN_LABEL, UI_FORM_FN_OPTIONS, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_FN_STYLES, UI_FORM_FN_SUBMIT_DISABLED, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_TITLE, UI_FORM_FN_VALUE, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_HIDDEN, UI_FORM_HINT, UI_FORM_LABEL_SINGULAR, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_PLACEHOLDER, UI_FORM_PREFIX, UI_FORM_PREFIX_ICON, UI_FORM_PREFIX_REF, UI_FORM_STYLES, UI_FORM_SUBMIT_TEXT, UI_FORM_SUFFIX, UI_FORM_SUFFIX_ICON, UI_FORM_SUFFIX_REF, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_COMPONENT, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_PREFIX, UI_TABLE_FN_STYLES, UI_TABLE_HIDDEN, UI_TABLE_ORDER, UI_TABLE_SELECT_WITH, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_WIDTH, UI_TYPE, ValueHelpClient, type ValueHelpInfo, type ValueHelpResult, type ValueHelpSearchOptions, WF_ACTION_WITH_DATA, asArray, buildDescendantErrorCounts, buildGridClasses, buildUnionVariants, createFieldValidator, createFormData, createFormDef, createFormValueResolver, createTableDef, defaultResolver, detectUnionVariant, enforceScale, extractLiteralOptions, extractMeasurement, extractValueHelp, formatDecimalForDisplay, getByPath, getColumn, getCurrencyDecimals, getCurrencyDisplayParts, getDecimalSeparator, getDeclaredFormActions, getDefaultClientFactory, getDefaultValidatorPlugins, getFieldMeta, getFilterableColumns, getFormValidator, getMetaEntry, getResolver, getSortableColumns, getThousandsSeparator, getVisibleColumns, groupInteger, hasComputedAnnotations, isArrayField, isObjectField, isPureLiteralUnion, isTupleField, isUnionField, iteratePathAncestors, joinDecimalString, mergeErrorMaps, optKey, optLabel, parseColSpan, parseDecimalInput, parseRowSpan, parseStaticAttrs, parseStaticOptions, resetDefaultClientFactory, resetMetaCache, resetValueHelpCache, resolveAttrs, resolveFieldProp, resolveFormProp, resolveGridSpec, resolveOptions, resolveSingularLabel, resolveStatic, resolveValueHelp, setByPath, setDefaultClientFactory, setDefaultValidatorPlugins, setResolver, splitDecimalString, str, valueHelpDictPaths };
982
+ export { type ClientFactory, type ColumnDef, type CurrencyDisplay, DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_COLUMN_PRECISION, DB_HTTP_PATH, DB_REL_FK, DB_UNIT, DB_UNIT_REF, DEFAULT_COL_SPAN, DEFAULT_ROW_SPAN, type DecimalParts, EXPECT_MAX_LENGTH, type FieldMeta, type FieldResolver, type FormActionInfo, type FormArrayFieldDef, type FormDef, type FormDiffOptions, type FormDiffResult, type FormFieldChange, type FormFieldDef, type FormObjectFieldDef, type FormTupleFieldDef, type FormUnionFieldDef, type FormUnionVariant, type FormatDecimalOptions, type GridSpanArgs, type GridSpec, META_DEFAULT, META_DESCRIPTION, META_ID, META_LABEL, META_READONLY, META_REQUIRED, META_SENSITIVE, type MeasurementInfo, type MetaCacheEntry, type MetaResponse, type PaginationControl, type RelationInfo, type ResolvedValueHelp, type SearchIndexInfo, type SortControl, StaticFieldResolver, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TFieldValidatorOptions, type TFormAction, type TFormEntryOptions, type TFormValidatorCallOptions, type TFormValueResolver, type TResolveOptions, type TableActionsModel, type TableDef, type TableQueryState, UI_DICT_ATTR, UI_DICT_DESCR, UI_DICT_FILTERABLE, UI_DICT_LABEL, UI_DICT_SEARCHABLE, UI_DICT_SORTABLE, UI_FORM_ACTION, UI_FORM_ATTR, UI_FORM_AUTOCOMPLETE, UI_FORM_CLASSES, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_CLASSES, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_HINT, UI_FORM_FN_LABEL, UI_FORM_FN_OPTIONS, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_FN_STYLES, UI_FORM_FN_SUBMIT_DISABLED, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_TITLE, UI_FORM_FN_VALUE, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_HIDDEN, UI_FORM_HINT, UI_FORM_LABEL_SINGULAR, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_PLACEHOLDER, UI_FORM_PREFIX, UI_FORM_PREFIX_ICON, UI_FORM_PREFIX_REF, UI_FORM_STYLES, UI_FORM_SUBMIT_TEXT, UI_FORM_SUFFIX, UI_FORM_SUFFIX_ICON, UI_FORM_SUFFIX_REF, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_COMPONENT, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_PREFIX, UI_TABLE_FN_STYLES, UI_TABLE_HIDDEN, UI_TABLE_ORDER, UI_TABLE_SELECT_WITH, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_WIDTH, UI_TYPE, ValueHelpClient, type ValueHelpInfo, type ValueHelpResult, type ValueHelpSearchOptions, WF_ACTION_WITH_DATA, asArray, buildDescendantErrorCounts, buildFormDiff, buildGridClasses, buildUnionVariants, createFieldValidator, createFormData, createFormDef, createFormValueResolver, createTableDef, defaultResolver, detectUnionVariant, enforceScale, extractLiteralOptions, extractMeasurement, extractValueHelp, formatDecimalForDisplay, getByPath, getColumn, getCurrencyDecimals, getCurrencyDisplayParts, getDecimalSeparator, getDeclaredFormActions, getDefaultClientFactory, getDefaultValidatorPlugins, getFieldMeta, getFilterableColumns, getFormValidator, getMetaEntry, getResolver, getSortableColumns, getThousandsSeparator, getVisibleColumns, groupInteger, hasComputedAnnotations, isArrayField, isObjectField, isPureLiteralUnion, isTupleField, isUnionField, iteratePathAncestors, joinDecimalString, mergeErrorMaps, optKey, optLabel, parseColSpan, parseDecimalInput, parseRowSpan, parseStaticAttrs, parseStaticOptions, resetDefaultClientFactory, resetMetaCache, resetValueHelpCache, resolveAttrs, resolveFieldProp, resolveFormProp, resolveGridSpec, resolveOptions, resolveSingularLabel, resolveStatic, resolveValueHelp, setByPath, setDefaultClientFactory, setDefaultValidatorPlugins, setResolver, splitDecimalString, str, valueHelpDictPaths };
package/dist/index.mjs CHANGED
@@ -947,6 +947,435 @@ function createFieldValidator(prop, opts) {
947
947
  };
948
948
  }
949
949
  //#endregion
950
+ //#region src/form/diff.ts
951
+ /**
952
+ * Diffs a form's `current` data against its `baseline` snapshot, producing both
953
+ * a changed-fields list and an `@atscript/db` patch object.
954
+ *
955
+ * Both `baseline` and `current` are the WRAPPED form-data container
956
+ * (`{ value: domainData }`) so this reuses {@link getByPath}.
957
+ *
958
+ * Revert-aware: a value edited back to its baseline produces no change and no
959
+ * patch entry.
960
+ *
961
+ * Snapshot contract: the result is NOT a deep copy. `$insert` items, `$replace`
962
+ * arrays, scalar leaf values, and `changes[].before/after` all hold live
963
+ * references into `baseline` / `current`. Callers that keep editing the form
964
+ * after building the patch must snapshot first (e.g. build the patch at submit
965
+ * time on a frozen clone). This is the common Vue v-model flow.
966
+ */
967
+ function buildFormDiff(def, baseline, current, opts) {
968
+ const changes = [];
969
+ const patch = {};
970
+ const versionColumn = findVersionColumn(def);
971
+ diffFields(def.fields, "", baseline, current, changes, patch, versionColumn, def.flatMap);
972
+ if ((opts?.cas ?? true) && versionColumn && Object.keys(patch).length > 0) {
973
+ const baselineVersion = getByPath(baseline, versionColumn);
974
+ if (typeof baselineVersion === "number" && Number.isInteger(baselineVersion)) patch.$cas = { [versionColumn]: baselineVersion };
975
+ }
976
+ return {
977
+ isDirty: changes.length > 0,
978
+ changes,
979
+ patch
980
+ };
981
+ }
982
+ /**
983
+ * Diffs a list of sibling fields. `prefix` is the dot-path of the parent
984
+ * context relative to the form root (used only for the change list `path`);
985
+ * patch entries are written into the local `patch` object so callers can place
986
+ * the whole sub-object as a nested partial.
987
+ *
988
+ * `versionColumn` is the top-level `@db.column.version` field name (or
989
+ * undefined). When set, the matching top-level field is skipped entirely — it
990
+ * is server-managed and may only be round-tripped via `$cas`.
991
+ *
992
+ * `inlineFlatMap` is the form's `flatMap`, supplied only at the top-level walk.
993
+ * It lets {@link diffScalarField} read `@db.patch.strategy` off the object
994
+ * ancestors of an INLINED leaf (a dotted-path leaf with no `FormObjectFieldDef`
995
+ * node — `createFormDef` dissolves unlabelled objects into dot-paths). At a
996
+ * default (replace) ancestor the whole sub-object must be emitted. It is
997
+ * intentionally NOT propagated into `diffObjectField` recursion, where the
998
+ * strategy decision is already made per structured object.
999
+ */
1000
+ function diffFields(fields, prefix, baseline, current, changes, patch, versionColumn, inlineFlatMap) {
1001
+ for (const field of fields) {
1002
+ if (field.path === "" && fields.length === 1) {
1003
+ diffLeafRoot(field, baseline, current, changes, patch);
1004
+ continue;
1005
+ }
1006
+ if (field.phantom) continue;
1007
+ if (versionColumn !== void 0 && !prefix && field.path === versionColumn) continue;
1008
+ const fullPath = prefix ? `${prefix}.${field.path}` : field.path;
1009
+ if (isArrayField(field)) {
1010
+ diffArrayField(field, fullPath, baseline, current, changes, patch);
1011
+ continue;
1012
+ }
1013
+ if (isObjectField(field)) {
1014
+ diffObjectField(field, fullPath, baseline, current, changes, patch);
1015
+ continue;
1016
+ }
1017
+ diffScalarField(field, fullPath, baseline, current, changes, patch, inlineFlatMap);
1018
+ }
1019
+ }
1020
+ /** Single-leaf root form (non-object root). Whole value is the patch. */
1021
+ function diffLeafRoot(field, baseline, current, changes, patch) {
1022
+ const before = getByPath(baseline, "");
1023
+ const after = getByPath(current, "");
1024
+ if (deepEqual(before, after)) return;
1025
+ if (isArrayField(field)) {
1026
+ const arrayPatch = diffArray(field, before, after);
1027
+ if (arrayPatch === void 0) return;
1028
+ changes.push({
1029
+ path: "",
1030
+ kind: "array",
1031
+ before,
1032
+ after
1033
+ });
1034
+ patch.value = arrayPatch;
1035
+ } else {
1036
+ changes.push({
1037
+ path: "",
1038
+ kind: "set",
1039
+ before,
1040
+ after
1041
+ });
1042
+ patch.value = after === void 0 ? null : after;
1043
+ }
1044
+ }
1045
+ /** Scalar / union / tuple / ref field — whole-value compare; clear → null. */
1046
+ function diffScalarField(field, fullPath, baseline, current, changes, patch, inlineFlatMap) {
1047
+ const before = getByPath(baseline, fullPath);
1048
+ const after = getByPath(current, fullPath);
1049
+ if (deepEqual(before, after)) return;
1050
+ changes.push({
1051
+ path: fullPath,
1052
+ kind: "set",
1053
+ before,
1054
+ after
1055
+ });
1056
+ if (inlineFlatMap && field.path.includes(".")) {
1057
+ const cutoff = inlinedReplaceCutoff(field.path, inlineFlatMap);
1058
+ if (cutoff !== void 0) {
1059
+ const prefixLen = fullPath.length - field.path.length;
1060
+ const sub = getByPath(current, prefixLen > 0 ? fullPath.slice(0, prefixLen) + cutoff : cutoff);
1061
+ setPatchLeaf(patch, cutoff, sub === void 0 ? null : sub);
1062
+ return;
1063
+ }
1064
+ }
1065
+ setPatchLeaf(patch, field.path, after === void 0 ? null : after);
1066
+ }
1067
+ /**
1068
+ * For a dotted INLINED leaf path, returns the path of the shallowest object
1069
+ * ancestor whose `@db.patch.strategy` is the default (`replace`), or undefined
1070
+ * when every ancestor is `merge` (then the leaf partial is correct). At a
1071
+ * replace ancestor the whole sub-object must be present in the patch.
1072
+ *
1073
+ * Walks ancestor segments (`a`, `a.b`, … but not the leaf itself); the first
1074
+ * one that is an object AND not merge is the cutoff. merge does NOT propagate,
1075
+ * so a default-replace level below a merge level still cuts off there.
1076
+ */
1077
+ function inlinedReplaceCutoff(leafPath, flatMap) {
1078
+ const segs = leafPath.split(".");
1079
+ let acc = "";
1080
+ for (let i = 0; i < segs.length - 1; i++) {
1081
+ acc = acc ? `${acc}.${segs[i]}` : segs[i];
1082
+ const prop = flatMap.get(acc);
1083
+ if (!prop || prop.type.kind !== "object") continue;
1084
+ if (getFieldMeta(prop, "db.patch.strategy") !== "merge") return acc;
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Inlined-or-structured object field — recurse, emit a nested partial OR the
1089
+ * whole sub-object depending on the field's `@db.patch.strategy`.
1090
+ *
1091
+ * atscript-db's DEFAULT nested-object patch strategy is `replace` (strict —
1092
+ * every required child must be present, else 400; omitted optionals are
1093
+ * null-filled). A changed-leaves-only partial is a valid patch ONLY when the
1094
+ * object field carries `@db.patch.strategy 'merge'`. For the default (replace)
1095
+ * case we therefore emit the WHOLE current sub-object so the validator passes
1096
+ * and no optional leaf is silently nulled. `merge` does NOT propagate, so a
1097
+ * descendant object without its own `merge` again emits its full sub-object
1098
+ * (handled by recursion — `diffFields` re-enters this function per child).
1099
+ *
1100
+ * Wholesale-clear: if the sub-object was a defined object in `baseline` but is
1101
+ * now undefined/null, emit `field: null` (object removed) instead of a partial
1102
+ * of nulled leaves.
1103
+ */
1104
+ function diffObjectField(field, fullPath, baseline, current, changes, patch) {
1105
+ const beforeObj = getByPath(baseline, fullPath);
1106
+ const afterObj = getByPath(current, fullPath);
1107
+ const nested = {};
1108
+ diffFields(field.objectDef.fields, fullPath, baseline, current, changes, nested, void 0, field.objectDef.flatMap);
1109
+ if ((afterObj === void 0 || afterObj === null) && isPlainObject(beforeObj)) {
1110
+ setPatchLeaf(patch, field.path, null);
1111
+ return;
1112
+ }
1113
+ if (Object.keys(nested).length === 0) return;
1114
+ if (getFieldMeta(field.prop, "db.patch.strategy") === "merge") setPatchLeaf(patch, field.path, nested);
1115
+ else setPatchLeaf(patch, field.path, afterObj === void 0 ? null : afterObj);
1116
+ }
1117
+ /** Array field — keyed → $update/$insert/$remove; unkeyed → $replace. */
1118
+ function diffArrayField(field, fullPath, baseline, current, changes, patch) {
1119
+ const before = getByPath(baseline, fullPath);
1120
+ const after = getByPath(current, fullPath);
1121
+ if (deepEqual(before, after)) return;
1122
+ const arrayPatch = diffArray(field, before, after);
1123
+ if (arrayPatch === void 0) return;
1124
+ changes.push({
1125
+ path: fullPath,
1126
+ kind: "array",
1127
+ before,
1128
+ after
1129
+ });
1130
+ setPatchLeaf(patch, field.path, arrayPatch);
1131
+ }
1132
+ /**
1133
+ * Produces a `TArrayPatch` value for one array field, or `undefined` when no
1134
+ * real op results (the caller then skips the field entirely).
1135
+ *
1136
+ * - Keyed arrays (item object has `@expect.array.key`): emit `$update`
1137
+ * (key + changed leaves), `$insert` (wholly-new items, whole), `$remove`
1138
+ * (key only). Reorder-only (same key membership, same content, different
1139
+ * order) → `$replace` (key-ops can't express a pure reorder).
1140
+ * Ambiguous keys (duplicate or missing key values) → `$replace` (the only
1141
+ * faithful op when key identity is unreliable).
1142
+ * - Unkeyed object arrays / primitive arrays: `$replace` with the whole array.
1143
+ * Primitive arrays with `@expect.array.uniqueItems` use by-value
1144
+ * `$insert` / `$remove` (set semantics).
1145
+ *
1146
+ * Deliberate `$insert`-not-`$upsert`: wholly-new keyed items use `$insert`
1147
+ * (pure append) rather than `$upsert`. This is safe because `$insert` is only
1148
+ * ever used for keys ABSENT from baseline; existing keys go through `$update`.
1149
+ * `$upsert` would dedupe-by-key, which we don't need given that invariant.
1150
+ */
1151
+ function diffArray(field, before, after) {
1152
+ const beforeArr = Array.isArray(before) ? before : [];
1153
+ const afterArr = Array.isArray(after) ? after : [];
1154
+ const keyProps = getArrayKeyProps(field.itemType);
1155
+ if (keyProps.length > 0) return diffKeyedArray(beforeArr, afterArr, keyProps);
1156
+ if (getFieldMeta(field.prop, "expect.array.uniqueItems") !== void 0 && isPrimitiveItem(field)) {
1157
+ const setPatch = diffUniqueArray(beforeArr, afterArr);
1158
+ if (setPatch) return setPatch;
1159
+ }
1160
+ return { $replace: afterArr };
1161
+ }
1162
+ /**
1163
+ * Keyed array diff. Reorder-only (same membership) falls back to $replace.
1164
+ *
1165
+ * Returns `undefined` when no $update/$insert/$remove op is produced (so the
1166
+ * caller skips the field rather than emitting a malformed empty `{}`).
1167
+ *
1168
+ * Ambiguity fallback: if either side has DUPLICATE key buckets, or ANY element
1169
+ * is missing all of its key values, key identity is unreliable — fall back to
1170
+ * `{ $replace: after }`, the only faithful op (last-write-wins collapse would
1171
+ * silently drop items, and a key-less `$update` is unmatchable by the DB).
1172
+ */
1173
+ function diffKeyedArray(before, after, keyProps) {
1174
+ if (hasKeylessItem(before, keyProps) || hasKeylessItem(after, keyProps)) return { $replace: after };
1175
+ const beforeByKey = /* @__PURE__ */ new Map();
1176
+ for (const el of before) if (isPlainObject(el)) beforeByKey.set(keyOf(el, keyProps), el);
1177
+ const afterByKey = /* @__PURE__ */ new Map();
1178
+ for (const el of after) if (isPlainObject(el)) afterByKey.set(keyOf(el, keyProps), el);
1179
+ if (beforeByKey.size !== before.length || afterByKey.size !== after.length) return { $replace: after };
1180
+ if (beforeByKey.size === afterByKey.size) {
1181
+ let sameMembershipAndContent = true;
1182
+ for (const [k, el] of afterByKey) {
1183
+ const prev = beforeByKey.get(k);
1184
+ if (prev === void 0 || !deepEqual(prev, el)) {
1185
+ sameMembershipAndContent = false;
1186
+ break;
1187
+ }
1188
+ }
1189
+ if (sameMembershipAndContent) return { $replace: after };
1190
+ }
1191
+ const $insert = [];
1192
+ const $update = [];
1193
+ const $remove = [];
1194
+ for (const [k, el] of afterByKey) {
1195
+ const prev = beforeByKey.get(k);
1196
+ if (prev === void 0) {
1197
+ $insert.push(el);
1198
+ continue;
1199
+ }
1200
+ if (!deepEqual(prev, el)) {
1201
+ const partial = buildKeyedUpdate(prev, el, keyProps);
1202
+ if (partial) $update.push(partial);
1203
+ }
1204
+ }
1205
+ for (const [k, el] of beforeByKey) if (!afterByKey.has(k)) $remove.push(pickKeys(el, keyProps));
1206
+ return arrayOps({
1207
+ $update,
1208
+ $insert,
1209
+ $remove
1210
+ });
1211
+ }
1212
+ /** Builds a `$update` partial: key fields + changed leaves only. */
1213
+ function buildKeyedUpdate(prev, next, keyProps) {
1214
+ const partial = {};
1215
+ for (const k of keyProps) partial[k] = next[k];
1216
+ let changed = false;
1217
+ const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
1218
+ for (const k of allKeys) {
1219
+ if (keyProps.includes(k)) continue;
1220
+ const a = prev[k];
1221
+ const b = next[k];
1222
+ if (!deepEqual(a, b)) {
1223
+ partial[k] = b === void 0 ? null : b;
1224
+ changed = true;
1225
+ }
1226
+ }
1227
+ return changed ? partial : void 0;
1228
+ }
1229
+ /** Primitive uniqueItems set diff. Returns undefined if neither side differs. */
1230
+ function diffUniqueArray(before, after) {
1231
+ const beforeSet = new Set(before.map((v) => stableKey(v)));
1232
+ const afterSet = new Set(after.map((v) => stableKey(v)));
1233
+ const $insert = [];
1234
+ const $remove = [];
1235
+ const seenInsert = /* @__PURE__ */ new Set();
1236
+ const seenRemove = /* @__PURE__ */ new Set();
1237
+ for (const v of after) {
1238
+ const k = stableKey(v);
1239
+ if (!beforeSet.has(k) && !seenInsert.has(k)) {
1240
+ $insert.push(v);
1241
+ seenInsert.add(k);
1242
+ }
1243
+ }
1244
+ for (const v of before) {
1245
+ const k = stableKey(v);
1246
+ if (!afterSet.has(k) && !seenRemove.has(k)) {
1247
+ $remove.push(v);
1248
+ seenRemove.add(k);
1249
+ }
1250
+ }
1251
+ return arrayOps({
1252
+ $insert,
1253
+ $remove
1254
+ });
1255
+ }
1256
+ /**
1257
+ * Assembles a `TArrayPatch` from named op-arrays, dropping empty ones. Returns
1258
+ * `undefined` when no op carries any item, so the caller skips the field rather
1259
+ * than emitting a malformed empty `{}`.
1260
+ */
1261
+ function arrayOps(ops) {
1262
+ const result = {};
1263
+ for (const k in ops) if (ops[k].length > 0) result[k] = ops[k];
1264
+ return Object.keys(result).length > 0 ? result : void 0;
1265
+ }
1266
+ /** Reads `@expect.array.key` props from an array item's object type. */
1267
+ function getArrayKeyProps(itemType) {
1268
+ if (itemType.type.kind !== "object") return [];
1269
+ const props = itemType.type.props;
1270
+ const keys = [];
1271
+ for (const [name, prop] of props.entries()) if (getFieldMeta(prop, "expect.array.key") !== void 0) keys.push(name);
1272
+ return keys;
1273
+ }
1274
+ /** True when the array's item type is a primitive (designType, kind === ''). */
1275
+ function isPrimitiveItem(field) {
1276
+ return field.itemType.type.kind === "";
1277
+ }
1278
+ /** Finds the form's `@db.column.version` column name, if any (top-level only). */
1279
+ function findVersionColumn(def) {
1280
+ for (const [path, prop] of def.flatMap.entries()) {
1281
+ if (!path || path.includes(".")) continue;
1282
+ if (getFieldMeta(prop, "db.column.version") !== void 0) return path;
1283
+ }
1284
+ }
1285
+ /** True when any element lacks ALL of its key values (un-keyable identity). */
1286
+ function hasKeylessItem(arr, keyProps) {
1287
+ for (const el of arr) {
1288
+ if (!isPlainObject(el)) return true;
1289
+ let hasAnyKey = false;
1290
+ for (const k of keyProps) {
1291
+ const v = el[k];
1292
+ if (v !== void 0 && v !== null) {
1293
+ hasAnyKey = true;
1294
+ break;
1295
+ }
1296
+ }
1297
+ if (!hasAnyKey) return true;
1298
+ }
1299
+ return false;
1300
+ }
1301
+ /** Composite key string for a keyed-array element. */
1302
+ function keyOf(el, keyProps) {
1303
+ if (keyProps.length === 1) return stableKey(el[keyProps[0]]);
1304
+ return keyProps.map((k) => stableKey(el[k])).join(" ");
1305
+ }
1306
+ /** Picks only the key fields from an element (for $remove). */
1307
+ function pickKeys(el, keyProps) {
1308
+ const out = {};
1309
+ for (const k of keyProps) out[k] = el[k];
1310
+ return out;
1311
+ }
1312
+ /** Writes a value into a (possibly dotted) leaf path on a local patch object. */
1313
+ function setPatchLeaf(patch, path, value) {
1314
+ if (!path.includes(".")) {
1315
+ patch[path] = value;
1316
+ return;
1317
+ }
1318
+ const keys = path.split(".");
1319
+ const last = keys.pop();
1320
+ let cur = patch;
1321
+ for (const k of keys) {
1322
+ let next = cur[k];
1323
+ if (next === void 0 || next === null || typeof next !== "object") {
1324
+ next = {};
1325
+ cur[k] = next;
1326
+ }
1327
+ cur = next;
1328
+ }
1329
+ cur[last] = value;
1330
+ }
1331
+ function isPlainObject(v) {
1332
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1333
+ }
1334
+ /**
1335
+ * Stable string key for primitives / values (used for set membership).
1336
+ *
1337
+ * Key equality is TYPE-STRICT: a number `1` and a string `'1'` produce
1338
+ * distinct keys, so a keyed-array item whose key changes JS representation
1339
+ * between baseline and current is treated as a remove + insert. Callers that
1340
+ * round-trip keys with loose typing should normalise the key type first.
1341
+ */
1342
+ function stableKey(v) {
1343
+ if (typeof v === "string") return `s:${v}`;
1344
+ if (typeof v === "number" || typeof v === "boolean") return `p:${String(v)}`;
1345
+ if (v === null) return "null";
1346
+ if (v === void 0) return "undef";
1347
+ return `j:${JSON.stringify(v)}`;
1348
+ }
1349
+ /**
1350
+ * Structural deep equality (order-sensitive for arrays). `NaN` equals `NaN`
1351
+ * (revert-aware for NaN scalars) while `0` / `-0` stay equal (matches DB
1352
+ * intent — `===` treats them equal, only NaN is special-cased).
1353
+ */
1354
+ function deepEqual(a, b) {
1355
+ if (a === b) return true;
1356
+ if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
1357
+ if (a === null || b === null || a === void 0 || b === void 0) return false;
1358
+ if (typeof a !== "object" || typeof b !== "object") return false;
1359
+ const aIsArr = Array.isArray(a);
1360
+ const bIsArr = Array.isArray(b);
1361
+ if (aIsArr !== bIsArr) return false;
1362
+ if (aIsArr && bIsArr) {
1363
+ if (a.length !== b.length) return false;
1364
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
1365
+ return true;
1366
+ }
1367
+ const ao = a;
1368
+ const bo = b;
1369
+ const aKeys = Object.keys(ao);
1370
+ const bKeys = Object.keys(bo);
1371
+ if (aKeys.length !== bKeys.length) return false;
1372
+ for (const k of aKeys) {
1373
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
1374
+ if (!deepEqual(ao[k], bo[k])) return false;
1375
+ }
1376
+ return true;
1377
+ }
1378
+ //#endregion
950
1379
  //#region src/form/error-utils.ts
951
1380
  /**
952
1381
  * Framework-agnostic helpers for working with form-error maps keyed by
@@ -1553,4 +1982,4 @@ function getColumn(def, path) {
1553
1982
  return def.columns.find((c) => c.path === path);
1554
1983
  }
1555
1984
  //#endregion
1556
- export { DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_COLUMN_PRECISION, DB_HTTP_PATH, DB_REL_FK, DB_UNIT, DB_UNIT_REF, DEFAULT_COL_SPAN, DEFAULT_ROW_SPAN, EXPECT_MAX_LENGTH, META_DEFAULT, META_DESCRIPTION, META_ID, META_LABEL, META_READONLY, META_REQUIRED, META_SENSITIVE, StaticFieldResolver, UI_DICT_ATTR, UI_DICT_DESCR, UI_DICT_FILTERABLE, UI_DICT_LABEL, UI_DICT_SEARCHABLE, UI_DICT_SORTABLE, UI_FORM_ACTION, UI_FORM_ATTR, UI_FORM_AUTOCOMPLETE, UI_FORM_CLASSES, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_CLASSES, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_HINT, UI_FORM_FN_LABEL, UI_FORM_FN_OPTIONS, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_FN_STYLES, UI_FORM_FN_SUBMIT_DISABLED, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_TITLE, UI_FORM_FN_VALUE, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_HIDDEN, UI_FORM_HINT, UI_FORM_LABEL_SINGULAR, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_PLACEHOLDER, UI_FORM_PREFIX, UI_FORM_PREFIX_ICON, UI_FORM_PREFIX_REF, UI_FORM_STYLES, UI_FORM_SUBMIT_TEXT, UI_FORM_SUFFIX, UI_FORM_SUFFIX_ICON, UI_FORM_SUFFIX_REF, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_COMPONENT, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_PREFIX, UI_TABLE_FN_STYLES, UI_TABLE_HIDDEN, UI_TABLE_ORDER, UI_TABLE_SELECT_WITH, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_WIDTH, UI_TYPE, ValueHelpClient, WF_ACTION_WITH_DATA, asArray, buildDescendantErrorCounts, buildGridClasses, buildUnionVariants, createFieldValidator, createFormData, createFormDef, createFormValueResolver, createTableDef, defaultResolver, detectUnionVariant, enforceScale, extractLiteralOptions, extractMeasurement, extractValueHelp, formatDecimalForDisplay, getByPath, getColumn, getCurrencyDecimals, getCurrencyDisplayParts, getDecimalSeparator, getDeclaredFormActions, getDefaultClientFactory, getDefaultValidatorPlugins, getFieldMeta, getFilterableColumns, getFormValidator, getMetaEntry, getResolver, getSortableColumns, getThousandsSeparator, getVisibleColumns, groupInteger, hasComputedAnnotations, isArrayField, isObjectField, isPureLiteralUnion, isTupleField, isUnionField, iteratePathAncestors, joinDecimalString, mergeErrorMaps, optKey, optLabel, parseColSpan, parseDecimalInput, parseRowSpan, parseStaticAttrs, parseStaticOptions, resetDefaultClientFactory, resetMetaCache, resetValueHelpCache, resolveAttrs, resolveFieldProp, resolveFormProp, resolveGridSpec, resolveOptions, resolveSingularLabel, resolveStatic, resolveValueHelp, setByPath, setDefaultClientFactory, setDefaultValidatorPlugins, setResolver, splitDecimalString, str, valueHelpDictPaths };
1985
+ export { DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_COLUMN_PRECISION, DB_HTTP_PATH, DB_REL_FK, DB_UNIT, DB_UNIT_REF, DEFAULT_COL_SPAN, DEFAULT_ROW_SPAN, EXPECT_MAX_LENGTH, META_DEFAULT, META_DESCRIPTION, META_ID, META_LABEL, META_READONLY, META_REQUIRED, META_SENSITIVE, StaticFieldResolver, UI_DICT_ATTR, UI_DICT_DESCR, UI_DICT_FILTERABLE, UI_DICT_LABEL, UI_DICT_SEARCHABLE, UI_DICT_SORTABLE, UI_FORM_ACTION, UI_FORM_ATTR, UI_FORM_AUTOCOMPLETE, UI_FORM_CLASSES, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_CLASSES, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_HINT, UI_FORM_FN_LABEL, UI_FORM_FN_OPTIONS, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_FN_STYLES, UI_FORM_FN_SUBMIT_DISABLED, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_TITLE, UI_FORM_FN_VALUE, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_HIDDEN, UI_FORM_HINT, UI_FORM_LABEL_SINGULAR, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_PLACEHOLDER, UI_FORM_PREFIX, UI_FORM_PREFIX_ICON, UI_FORM_PREFIX_REF, UI_FORM_STYLES, UI_FORM_SUBMIT_TEXT, UI_FORM_SUFFIX, UI_FORM_SUFFIX_ICON, UI_FORM_SUFFIX_REF, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_COMPONENT, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_PREFIX, UI_TABLE_FN_STYLES, UI_TABLE_HIDDEN, UI_TABLE_ORDER, UI_TABLE_SELECT_WITH, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_WIDTH, UI_TYPE, ValueHelpClient, WF_ACTION_WITH_DATA, asArray, buildDescendantErrorCounts, buildFormDiff, buildGridClasses, buildUnionVariants, createFieldValidator, createFormData, createFormDef, createFormValueResolver, createTableDef, defaultResolver, detectUnionVariant, enforceScale, extractLiteralOptions, extractMeasurement, extractValueHelp, formatDecimalForDisplay, getByPath, getColumn, getCurrencyDecimals, getCurrencyDisplayParts, getDecimalSeparator, getDeclaredFormActions, getDefaultClientFactory, getDefaultValidatorPlugins, getFieldMeta, getFilterableColumns, getFormValidator, getMetaEntry, getResolver, getSortableColumns, getThousandsSeparator, getVisibleColumns, groupInteger, hasComputedAnnotations, isArrayField, isObjectField, isPureLiteralUnion, isTupleField, isUnionField, iteratePathAncestors, joinDecimalString, mergeErrorMaps, optKey, optLabel, parseColSpan, parseDecimalInput, parseRowSpan, parseStaticAttrs, parseStaticOptions, resetDefaultClientFactory, resetMetaCache, resetValueHelpCache, resolveAttrs, resolveFieldProp, resolveFormProp, resolveGridSpec, resolveOptions, resolveSingularLabel, resolveStatic, resolveValueHelp, setByPath, setDefaultClientFactory, setDefaultValidatorPlugins, setResolver, splitDecimalString, str, valueHelpDictPaths };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/ui",
3
- "version": "0.1.102",
3
+ "version": "0.1.103",
4
4
  "description": "Framework-agnostic runtime for form and table definitions from atscript annotated types",
5
5
  "keywords": [
6
6
  "annotations",