js-bao 0.5.0 → 0.5.1

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.
@@ -84,9 +84,25 @@ var init_DocumentQueryTranslator = __esm({
84
84
  }
85
85
  });
86
86
 
87
+ // src/utils/yjsValues.ts
88
+ function isStringSetYMap(value) {
89
+ if (!(value instanceof Y.Map)) return false;
90
+ for (const v of value.values()) {
91
+ if (v !== true) return false;
92
+ }
93
+ return true;
94
+ }
95
+ var Y;
96
+ var init_yjsValues = __esm({
97
+ "src/utils/yjsValues.ts"() {
98
+ "use strict";
99
+ Y = __toESM(require("yjs"), 1);
100
+ }
101
+ });
102
+
87
103
  // src/models/metaSync.ts
88
104
  function inferFieldType(value) {
89
- if (value instanceof Y.Map) return "stringset";
105
+ if (isStringSetYMap(value)) return "stringset";
90
106
  switch (typeof value) {
91
107
  case "string":
92
108
  return "string";
@@ -131,7 +147,7 @@ function syncModelMeta(yDoc, modelName, schema) {
131
147
  if (compoundConstraints.length > 0) {
132
148
  let constraints = meta.get("_constraints");
133
149
  if (!constraints) {
134
- constraints = new Y.Map();
150
+ constraints = new Y2.Map();
135
151
  meta.set("_constraints", constraints);
136
152
  }
137
153
  for (const constraint of compoundConstraints) {
@@ -142,7 +158,7 @@ function syncModelMeta(yDoc, modelName, schema) {
142
158
  if (relationships && Object.keys(relationships).length > 0) {
143
159
  let rels = meta.get("_relationships");
144
160
  if (!rels) {
145
- rels = new Y.Map();
161
+ rels = new Y2.Map();
146
162
  meta.set("_relationships", rels);
147
163
  }
148
164
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -154,7 +170,7 @@ function syncModelMeta(yDoc, modelName, schema) {
154
170
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
155
171
  let fieldMeta = metaMap.get(fieldName);
156
172
  if (!fieldMeta) {
157
- fieldMeta = new Y.Map();
173
+ fieldMeta = new Y2.Map();
158
174
  metaMap.set(fieldName, fieldMeta);
159
175
  }
160
176
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -162,6 +178,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
162
178
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
163
179
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
164
180
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
181
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
165
182
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
166
183
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
167
184
  const encoded = encodeDefault(fieldOpts.default);
@@ -170,7 +187,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
170
187
  function syncConstraintMeta(constraintsMap, constraint) {
171
188
  let cMeta = constraintsMap.get(constraint.name);
172
189
  if (!cMeta) {
173
- cMeta = new Y.Map();
190
+ cMeta = new Y2.Map();
174
191
  constraintsMap.set(constraint.name, cMeta);
175
192
  }
176
193
  setIfChanged(cMeta, "type", "unique");
@@ -180,7 +197,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
180
197
  function syncRelationshipMeta(relsMap, relName, relConfig) {
181
198
  let relMeta = relsMap.get(relName);
182
199
  if (!relMeta) {
183
- relMeta = new Y.Map();
200
+ relMeta = new Y2.Map();
184
201
  relsMap.set(relName, relMeta);
185
202
  }
186
203
  for (const [key, value] of Object.entries(relConfig)) {
@@ -195,7 +212,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
195
212
  if (fieldName.startsWith("_")) continue;
196
213
  let fieldMeta = meta.get(fieldName);
197
214
  if (!fieldMeta) {
198
- fieldMeta = new Y.Map();
215
+ fieldMeta = new Y2.Map();
199
216
  meta.set(fieldName, fieldMeta);
200
217
  }
201
218
  if (!fieldMeta.has("type")) {
@@ -211,11 +228,12 @@ function setIfChanged(map, key, value) {
211
228
  map.set(key, value);
212
229
  }
213
230
  }
214
- var Y, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
231
+ var Y2, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
215
232
  var init_metaSync = __esm({
216
233
  "src/models/metaSync.ts"() {
217
234
  "use strict";
218
- Y = __toESM(require("yjs"), 1);
235
+ Y2 = __toESM(require("yjs"), 1);
236
+ init_yjsValues();
219
237
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
220
238
  _syncedCache = /* @__PURE__ */ new WeakMap();
221
239
  }
@@ -225,11 +243,11 @@ var init_metaSync = __esm({
225
243
  function generateULID() {
226
244
  return (0, import_ulid.ulid)();
227
245
  }
228
- var Y2, import_ulid, Logger;
246
+ var Y3, import_ulid, Logger;
229
247
  var init_BaseModel = __esm({
230
248
  "src/models/BaseModel.ts"() {
231
249
  "use strict";
232
- Y2 = __toESM(require("yjs"), 1);
250
+ Y3 = __toESM(require("yjs"), 1);
233
251
  import_ulid = require("ulid");
234
252
  init_StringSet();
235
253
  init_documentTypes();
@@ -237,6 +255,7 @@ var init_BaseModel = __esm({
237
255
  init_CursorManager();
238
256
  init_sql();
239
257
  init_metaSync();
258
+ init_yjsValues();
240
259
  Logger = class {
241
260
  static _logLevel = 1 /* ERROR */;
242
261
  static _logCallback = null;
@@ -1223,7 +1242,8 @@ function connectDoDb(options) {
1223
1242
  }
1224
1243
 
1225
1244
  // src/utils/yDocSchema.ts
1226
- var Y3 = __toESM(require("yjs"), 1);
1245
+ var Y4 = __toESM(require("yjs"), 1);
1246
+ init_yjsValues();
1227
1247
  function discoverSchema(yDoc) {
1228
1248
  const models = {};
1229
1249
  const metaNames = /* @__PURE__ */ new Set();
@@ -1257,7 +1277,7 @@ function discoverModelNames(yDoc) {
1257
1277
  function materializeMap(yDoc, key) {
1258
1278
  try {
1259
1279
  const map = yDoc.getMap(key);
1260
- return map instanceof Y3.Map ? map : null;
1280
+ return map instanceof Y4.Map ? map : null;
1261
1281
  } catch {
1262
1282
  return null;
1263
1283
  }
@@ -1267,11 +1287,11 @@ function readModelMeta(metaMap) {
1267
1287
  let constraints;
1268
1288
  let relationships;
1269
1289
  for (const [key, value] of metaMap.entries()) {
1270
- if (key === "_constraints" && value instanceof Y3.Map) {
1290
+ if (key === "_constraints" && value instanceof Y4.Map) {
1271
1291
  constraints = readConstraints(value);
1272
- } else if (key === "_relationships" && value instanceof Y3.Map) {
1292
+ } else if (key === "_relationships" && value instanceof Y4.Map) {
1273
1293
  relationships = readRelationships(value);
1274
- } else if (value instanceof Y3.Map) {
1294
+ } else if (value instanceof Y4.Map) {
1275
1295
  fields[key] = readFieldMeta(value);
1276
1296
  }
1277
1297
  }
@@ -1290,6 +1310,10 @@ function readFieldMeta(fieldMap) {
1290
1310
  if (fieldMap.get("unique") === true) field.unique = true;
1291
1311
  if (fieldMap.get("required") === true) field.required = true;
1292
1312
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
1313
+ const autoStamp = fieldMap.get("autoStamp");
1314
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
1315
+ field.autoStamp = autoStamp;
1316
+ }
1293
1317
  const def = fieldMap.get("default");
1294
1318
  if (def !== void 0) field.default = def;
1295
1319
  const maxLength = fieldMap.get("maxLength");
@@ -1302,7 +1326,7 @@ function inferModelFromData(dataMap) {
1302
1326
  const fields = {};
1303
1327
  let sampled = 0;
1304
1328
  for (const [_recordId, recordValue] of dataMap.entries()) {
1305
- if (!(recordValue instanceof Y3.Map)) continue;
1329
+ if (!(recordValue instanceof Y4.Map)) continue;
1306
1330
  if (++sampled > 5) break;
1307
1331
  for (const [fieldName, value] of recordValue.entries()) {
1308
1332
  if (fieldName.startsWith("_")) continue;
@@ -1315,7 +1339,7 @@ function inferModelFromData(dataMap) {
1315
1339
  return { fields };
1316
1340
  }
1317
1341
  function inferTypeFromValue(value) {
1318
- if (value instanceof Y3.Map) return "stringset";
1342
+ if (isStringSetYMap(value)) return "stringset";
1319
1343
  switch (typeof value) {
1320
1344
  case "string":
1321
1345
  return "string";
@@ -1330,7 +1354,7 @@ function inferTypeFromValue(value) {
1330
1354
  function readConstraints(constraintsMap) {
1331
1355
  const out = {};
1332
1356
  for (const [name, value] of constraintsMap.entries()) {
1333
- if (!(value instanceof Y3.Map)) continue;
1357
+ if (!(value instanceof Y4.Map)) continue;
1334
1358
  let fields = [];
1335
1359
  const rawFields = value.get("fields");
1336
1360
  if (typeof rawFields === "string") {
@@ -1349,7 +1373,7 @@ function readConstraints(constraintsMap) {
1349
1373
  function readRelationships(relsMap) {
1350
1374
  const out = {};
1351
1375
  for (const [name, value] of relsMap.entries()) {
1352
- if (!(value instanceof Y3.Map)) continue;
1376
+ if (!(value instanceof Y4.Map)) continue;
1353
1377
  const rel = {};
1354
1378
  for (const [k, v] of value.entries()) {
1355
1379
  rel[k] = v;
@@ -1360,6 +1384,7 @@ function readRelationships(relsMap) {
1360
1384
  }
1361
1385
  var CAMEL_TO_SNAKE = {
1362
1386
  autoAssign: "auto_assign",
1387
+ autoStamp: "auto_stamp",
1363
1388
  maxLength: "max_length",
1364
1389
  maxCount: "max_count",
1365
1390
  relatedIdField: "related_id_field",
@@ -1390,6 +1415,7 @@ function schemaToToml(schema) {
1390
1415
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
1391
1416
  lines.push(`type = ${tomlValue(field.type)}`);
1392
1417
  if (field.autoAssign) lines.push("auto_assign = true");
1418
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
1393
1419
  if (field.indexed) lines.push("indexed = true");
1394
1420
  if (field.unique) lines.push("unique = true");
1395
1421
  if (field.required) lines.push("required = true");
@@ -1513,10 +1539,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
1513
1539
  "unique",
1514
1540
  "required",
1515
1541
  "auto_assign",
1542
+ "auto_stamp",
1516
1543
  "max_length",
1517
1544
  "max_count",
1518
- "default"
1545
+ "default",
1546
+ "enum"
1519
1547
  ]);
1548
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
1520
1549
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
1521
1550
  "fields",
1522
1551
  "relationships",
@@ -1558,9 +1587,37 @@ function parseFieldOptions(raw, context, strict) {
1558
1587
  if (raw.unique === true) opts.unique = true;
1559
1588
  if (raw.required === true) opts.required = true;
1560
1589
  if (raw.auto_assign === true) opts.autoAssign = true;
1590
+ if (raw.auto_stamp !== void 0) {
1591
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
1592
+ throw new Error(
1593
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
1594
+ ...VALID_AUTO_STAMP_VALUES
1595
+ ].join(", ")}`
1596
+ );
1597
+ }
1598
+ opts.autoStamp = raw.auto_stamp;
1599
+ }
1561
1600
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
1562
1601
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
1563
1602
  if (raw.default !== void 0) opts.default = raw.default;
1603
+ if (raw.enum !== void 0) {
1604
+ if (raw.type !== "string") {
1605
+ throw new Error(
1606
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
1607
+ );
1608
+ }
1609
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
1610
+ throw new Error(
1611
+ `${context}: \`enum\` must be a non-empty array of strings.`
1612
+ );
1613
+ }
1614
+ if (!raw.enum.every((v) => typeof v === "string")) {
1615
+ throw new Error(
1616
+ `${context}: \`enum\` values must all be strings.`
1617
+ );
1618
+ }
1619
+ opts.enum = [...raw.enum];
1620
+ }
1564
1621
  return opts;
1565
1622
  }
1566
1623
  function requireField(raw, field, context) {
@@ -31,8 +31,32 @@ interface FieldOptions {
31
31
  unique?: boolean;
32
32
  default?: any | (() => any);
33
33
  autoAssign?: boolean;
34
+ /**
35
+ * Auto-timestamp this field with `Date.now()` (milliseconds) on save.
36
+ *
37
+ * - `'create'` — stamp only on the first save (when `isNew === true`).
38
+ * - `'update'` — stamp on every save (create AND update).
39
+ * - `'both'` — stamp on every save (create AND update).
40
+ *
41
+ * If the caller passes an explicit value for the field in `data`, the
42
+ * explicit value wins and the stamp is skipped. The stamp is applied
43
+ * in `beforeSave` BEFORE any user-defined hooks, so user hooks may
44
+ * still overwrite the value if they wish (last writer wins).
45
+ */
46
+ autoStamp?: "create" | "update" | "both";
34
47
  maxLength?: number;
35
48
  maxCount?: number;
49
+ /**
50
+ * Allowed-value set for a `string` field. When present, the codegen
51
+ * generators (database-type codegen + doc-model v2 codegen) emit a
52
+ * TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
53
+ * `string` for this field.
54
+ *
55
+ * Advisory / codegen-only: this is a TS-emission hint. The runtime and the
56
+ * server do NOT enforce enum membership on write (see #843). Only valid on
57
+ * `string` fields, and must be a non-empty array of strings.
58
+ */
59
+ enum?: string[];
36
60
  }
37
61
  interface UniqueConstraintConfig {
38
62
  name: string;
@@ -394,6 +418,17 @@ declare class BaseModel implements StringSetChangeTracker {
394
418
  private ensureLocalChanges;
395
419
  private hasLocalChange;
396
420
  private getFromYjs;
421
+ /**
422
+ * Defensive normalization for the read/sync boundary (issue #625).
423
+ *
424
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
425
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
426
+ * through untouched. For any other field, a composite Yjs primitive
427
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
428
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
429
+ * warn, rather than throwing. Scalars pass through unchanged.
430
+ */
431
+ private normalizeNonStringSetValue;
397
432
  private getValue;
398
433
  private setValue;
399
434
  get isDirty(): boolean;
@@ -754,6 +789,25 @@ interface SaveRequest {
754
789
  ifNotExists?: boolean;
755
790
  condition?: DocumentFilter;
756
791
  upsertOn?: string;
792
+ /**
793
+ * Per-call auto-timestamp directives for schemaless writes.
794
+ *
795
+ * Each entry is a list of field names to stamp with `Date.now()`
796
+ * (milliseconds) at the corresponding lifecycle event:
797
+ *
798
+ * - `create` — field is stamped only when the record is new
799
+ * (no row exists yet for this `modelName` + `id`).
800
+ * - `update` — field is stamped on every save (create AND update).
801
+ *
802
+ * If the caller already provided an explicit value for the field
803
+ * in `data`, the explicit value wins and the stamp is skipped.
804
+ * Stamping happens BEFORE the `beforeSave` user hook runs, so user
805
+ * hooks may still overwrite the stamped value if they wish.
806
+ */
807
+ autoStamps?: {
808
+ create?: string[];
809
+ update?: string[];
810
+ };
757
811
  }
758
812
  interface DeleteRequest {
759
813
  modelName: string;
@@ -862,6 +916,17 @@ interface PatchRequest {
862
916
  data: Record<string, any>;
863
917
  stringSets?: Record<string, string[]>;
864
918
  condition?: DocumentFilter;
919
+ /**
920
+ * Per-call auto-timestamp directives. A patch is always treated as
921
+ * an update (`isNew === false`), so only the `update` list is
922
+ * applied; the `create` list is ignored on patch.
923
+ *
924
+ * Explicit values in `data` win.
925
+ */
926
+ autoStamps?: {
927
+ create?: string[];
928
+ update?: string[];
929
+ };
865
930
  }
866
931
  interface PatchResponse {
867
932
  success: boolean;
@@ -877,6 +942,19 @@ interface BatchOperation {
877
942
  ifNotExists?: boolean;
878
943
  condition?: DocumentFilter;
879
944
  upsertOn?: string;
945
+ /**
946
+ * Per-operation auto-timestamp directives. Same semantics as
947
+ * `SaveRequest.autoStamps` / `PatchRequest.autoStamps`. Applied
948
+ * BEFORE the user `beforeSave` hook runs, with explicit values
949
+ * in `data` always winning.
950
+ *
951
+ * For `op: "patch"`, only the `update` list applies (patches are
952
+ * always treated as updates).
953
+ */
954
+ autoStamps?: {
955
+ create?: string[];
956
+ update?: string[];
957
+ };
880
958
  }
881
959
  interface BatchRequest {
882
960
  operations: BatchOperation[];
@@ -1435,6 +1513,7 @@ interface DiscoveredField {
1435
1513
  required?: boolean;
1436
1514
  default?: string | number | boolean;
1437
1515
  autoAssign?: boolean;
1516
+ autoStamp?: "create" | "update" | "both";
1438
1517
  maxLength?: number;
1439
1518
  maxCount?: number;
1440
1519
  }
@@ -1541,6 +1620,11 @@ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSche
1541
1620
 
1542
1621
  /**
1543
1622
  * Infer a _meta_ type string from a JS runtime value.
1623
+ *
1624
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
1625
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
1626
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
1627
+ * than being mis-tagged.
1544
1628
  */
1545
1629
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
1546
1630
  /**
@@ -31,8 +31,32 @@ interface FieldOptions {
31
31
  unique?: boolean;
32
32
  default?: any | (() => any);
33
33
  autoAssign?: boolean;
34
+ /**
35
+ * Auto-timestamp this field with `Date.now()` (milliseconds) on save.
36
+ *
37
+ * - `'create'` — stamp only on the first save (when `isNew === true`).
38
+ * - `'update'` — stamp on every save (create AND update).
39
+ * - `'both'` — stamp on every save (create AND update).
40
+ *
41
+ * If the caller passes an explicit value for the field in `data`, the
42
+ * explicit value wins and the stamp is skipped. The stamp is applied
43
+ * in `beforeSave` BEFORE any user-defined hooks, so user hooks may
44
+ * still overwrite the value if they wish (last writer wins).
45
+ */
46
+ autoStamp?: "create" | "update" | "both";
34
47
  maxLength?: number;
35
48
  maxCount?: number;
49
+ /**
50
+ * Allowed-value set for a `string` field. When present, the codegen
51
+ * generators (database-type codegen + doc-model v2 codegen) emit a
52
+ * TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
53
+ * `string` for this field.
54
+ *
55
+ * Advisory / codegen-only: this is a TS-emission hint. The runtime and the
56
+ * server do NOT enforce enum membership on write (see #843). Only valid on
57
+ * `string` fields, and must be a non-empty array of strings.
58
+ */
59
+ enum?: string[];
36
60
  }
37
61
  interface UniqueConstraintConfig {
38
62
  name: string;
@@ -394,6 +418,17 @@ declare class BaseModel implements StringSetChangeTracker {
394
418
  private ensureLocalChanges;
395
419
  private hasLocalChange;
396
420
  private getFromYjs;
421
+ /**
422
+ * Defensive normalization for the read/sync boundary (issue #625).
423
+ *
424
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
425
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
426
+ * through untouched. For any other field, a composite Yjs primitive
427
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
428
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
429
+ * warn, rather than throwing. Scalars pass through unchanged.
430
+ */
431
+ private normalizeNonStringSetValue;
397
432
  private getValue;
398
433
  private setValue;
399
434
  get isDirty(): boolean;
@@ -754,6 +789,25 @@ interface SaveRequest {
754
789
  ifNotExists?: boolean;
755
790
  condition?: DocumentFilter;
756
791
  upsertOn?: string;
792
+ /**
793
+ * Per-call auto-timestamp directives for schemaless writes.
794
+ *
795
+ * Each entry is a list of field names to stamp with `Date.now()`
796
+ * (milliseconds) at the corresponding lifecycle event:
797
+ *
798
+ * - `create` — field is stamped only when the record is new
799
+ * (no row exists yet for this `modelName` + `id`).
800
+ * - `update` — field is stamped on every save (create AND update).
801
+ *
802
+ * If the caller already provided an explicit value for the field
803
+ * in `data`, the explicit value wins and the stamp is skipped.
804
+ * Stamping happens BEFORE the `beforeSave` user hook runs, so user
805
+ * hooks may still overwrite the stamped value if they wish.
806
+ */
807
+ autoStamps?: {
808
+ create?: string[];
809
+ update?: string[];
810
+ };
757
811
  }
758
812
  interface DeleteRequest {
759
813
  modelName: string;
@@ -862,6 +916,17 @@ interface PatchRequest {
862
916
  data: Record<string, any>;
863
917
  stringSets?: Record<string, string[]>;
864
918
  condition?: DocumentFilter;
919
+ /**
920
+ * Per-call auto-timestamp directives. A patch is always treated as
921
+ * an update (`isNew === false`), so only the `update` list is
922
+ * applied; the `create` list is ignored on patch.
923
+ *
924
+ * Explicit values in `data` win.
925
+ */
926
+ autoStamps?: {
927
+ create?: string[];
928
+ update?: string[];
929
+ };
865
930
  }
866
931
  interface PatchResponse {
867
932
  success: boolean;
@@ -877,6 +942,19 @@ interface BatchOperation {
877
942
  ifNotExists?: boolean;
878
943
  condition?: DocumentFilter;
879
944
  upsertOn?: string;
945
+ /**
946
+ * Per-operation auto-timestamp directives. Same semantics as
947
+ * `SaveRequest.autoStamps` / `PatchRequest.autoStamps`. Applied
948
+ * BEFORE the user `beforeSave` hook runs, with explicit values
949
+ * in `data` always winning.
950
+ *
951
+ * For `op: "patch"`, only the `update` list applies (patches are
952
+ * always treated as updates).
953
+ */
954
+ autoStamps?: {
955
+ create?: string[];
956
+ update?: string[];
957
+ };
880
958
  }
881
959
  interface BatchRequest {
882
960
  operations: BatchOperation[];
@@ -1435,6 +1513,7 @@ interface DiscoveredField {
1435
1513
  required?: boolean;
1436
1514
  default?: string | number | boolean;
1437
1515
  autoAssign?: boolean;
1516
+ autoStamp?: "create" | "update" | "both";
1438
1517
  maxLength?: number;
1439
1518
  maxCount?: number;
1440
1519
  }
@@ -1541,6 +1620,11 @@ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSche
1541
1620
 
1542
1621
  /**
1543
1622
  * Infer a _meta_ type string from a JS runtime value.
1623
+ *
1624
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
1625
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
1626
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
1627
+ * than being mis-tagged.
1544
1628
  */
1545
1629
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
1546
1630
  /**