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.
@@ -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;
@@ -1023,6 +1047,25 @@ interface SaveRequest {
1023
1047
  ifNotExists?: boolean;
1024
1048
  condition?: DocumentFilter;
1025
1049
  upsertOn?: string;
1050
+ /**
1051
+ * Per-call auto-timestamp directives for schemaless writes.
1052
+ *
1053
+ * Each entry is a list of field names to stamp with `Date.now()`
1054
+ * (milliseconds) at the corresponding lifecycle event:
1055
+ *
1056
+ * - `create` — field is stamped only when the record is new
1057
+ * (no row exists yet for this `modelName` + `id`).
1058
+ * - `update` — field is stamped on every save (create AND update).
1059
+ *
1060
+ * If the caller already provided an explicit value for the field
1061
+ * in `data`, the explicit value wins and the stamp is skipped.
1062
+ * Stamping happens BEFORE the `beforeSave` user hook runs, so user
1063
+ * hooks may still overwrite the stamped value if they wish.
1064
+ */
1065
+ autoStamps?: {
1066
+ create?: string[];
1067
+ update?: string[];
1068
+ };
1026
1069
  }
1027
1070
  interface DeleteRequest {
1028
1071
  modelName: string;
@@ -1131,6 +1174,17 @@ interface PatchRequest {
1131
1174
  data: Record<string, any>;
1132
1175
  stringSets?: Record<string, string[]>;
1133
1176
  condition?: DocumentFilter;
1177
+ /**
1178
+ * Per-call auto-timestamp directives. A patch is always treated as
1179
+ * an update (`isNew === false`), so only the `update` list is
1180
+ * applied; the `create` list is ignored on patch.
1181
+ *
1182
+ * Explicit values in `data` win.
1183
+ */
1184
+ autoStamps?: {
1185
+ create?: string[];
1186
+ update?: string[];
1187
+ };
1134
1188
  }
1135
1189
  interface PatchResponse {
1136
1190
  success: boolean;
@@ -1146,6 +1200,19 @@ interface BatchOperation {
1146
1200
  ifNotExists?: boolean;
1147
1201
  condition?: DocumentFilter;
1148
1202
  upsertOn?: string;
1203
+ /**
1204
+ * Per-operation auto-timestamp directives. Same semantics as
1205
+ * `SaveRequest.autoStamps` / `PatchRequest.autoStamps`. Applied
1206
+ * BEFORE the user `beforeSave` hook runs, with explicit values
1207
+ * in `data` always winning.
1208
+ *
1209
+ * For `op: "patch"`, only the `update` list applies (patches are
1210
+ * always treated as updates).
1211
+ */
1212
+ autoStamps?: {
1213
+ create?: string[];
1214
+ update?: string[];
1215
+ };
1149
1216
  }
1150
1217
  interface BatchRequest {
1151
1218
  operations: BatchOperation[];
@@ -1186,6 +1253,7 @@ interface SchemaFieldInfo {
1186
1253
  required?: boolean;
1187
1254
  default?: string | number | boolean;
1188
1255
  autoAssign?: boolean;
1256
+ autoStamp?: "create" | "update" | "both";
1189
1257
  maxLength?: number;
1190
1258
  maxCount?: number;
1191
1259
  }
@@ -1207,6 +1275,35 @@ interface SchemaModelInfo {
1207
1275
  interface SchemaResponse {
1208
1276
  models: Record<string, SchemaModelInfo>;
1209
1277
  }
1278
+ /**
1279
+ * Return an error message if `fieldName` is a reserved field name
1280
+ * (matches the policy enforced by `_checkReservedFields` on incoming
1281
+ * record data). Returns `null` if the name is acceptable.
1282
+ *
1283
+ * "id" is allowed (stripped before storage in the JSON column).
1284
+ * "type" maps to the internal `_type` column. Any name starting with
1285
+ * "_" is internal.
1286
+ *
1287
+ * @internal
1288
+ */
1289
+ declare function checkReservedFieldName(fieldName: string): string | null;
1290
+ /**
1291
+ * Validate that none of the field names listed in `autoStamps.create` /
1292
+ * `autoStamps.update` are reserved. Returns an error message on the
1293
+ * first violation, or `null` if everything is acceptable.
1294
+ *
1295
+ * Codex P2 #2: `_checkReservedFields` only inspects the keys already
1296
+ * present in `data`. Without this check, a request such as
1297
+ * `{ autoStamps: { update: ["type"] } }` would pass the data-level
1298
+ * guard and then have `applyAutoStamps` add a reserved key after the
1299
+ * fact, bypassing the protection.
1300
+ *
1301
+ * @internal
1302
+ */
1303
+ declare function checkAutoStampsReservedFields(autoStamps: {
1304
+ create?: string[];
1305
+ update?: string[];
1306
+ } | undefined): string | null;
1210
1307
  /**
1211
1308
  * Create a Durable Object class for schemaless database storage.
1212
1309
  *
@@ -1432,6 +1529,7 @@ interface DiscoveredField {
1432
1529
  required?: boolean;
1433
1530
  default?: string | number | boolean;
1434
1531
  autoAssign?: boolean;
1532
+ autoStamp?: "create" | "update" | "both";
1435
1533
  maxLength?: number;
1436
1534
  maxCount?: number;
1437
1535
  }
@@ -1676,6 +1774,17 @@ declare class BaseModel implements StringSetChangeTracker {
1676
1774
  private ensureLocalChanges;
1677
1775
  private hasLocalChange;
1678
1776
  private getFromYjs;
1777
+ /**
1778
+ * Defensive normalization for the read/sync boundary (issue #625).
1779
+ *
1780
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
1781
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
1782
+ * through untouched. For any other field, a composite Yjs primitive
1783
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
1784
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
1785
+ * warn, rather than throwing. Scalars pass through unchanged.
1786
+ */
1787
+ private normalizeNonStringSetValue;
1679
1788
  private getValue;
1680
1789
  private setValue;
1681
1790
  get isDirty(): boolean;
@@ -1910,6 +2019,11 @@ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSche
1910
2019
 
1911
2020
  /**
1912
2021
  * Infer a _meta_ type string from a JS runtime value.
2022
+ *
2023
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
2024
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
2025
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
2026
+ * than being mis-tagged.
1913
2027
  */
1914
2028
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
1915
2029
  /**
@@ -1941,4 +2055,16 @@ declare function syncModelMeta(yDoc: Y.Doc, modelName: string, schema: ModelSche
1941
2055
  */
1942
2056
  declare function syncInferredMeta(yDoc: Y.Doc, modelName: string, recordData: Record<string, any>): void;
1943
2057
 
1944
- export { type AfterQueryContext, type AfterQueryResult, type AggregateRequest, type AggregateResponse, type AggregationOperation$1 as AggregationOperation, type AggregationOptions$1 as AggregationOptions, type AggregationResult$1 as AggregationResult, type BatchOperation, type BatchOperationResult, type BatchRequest, type BatchResponse, type BeforeDeleteContext, type BeforeQueryContext, type BeforeQueryResult, type BeforeSaveContext, type CountRequest, type CountResponse, type DatabaseDOConfig, type DatabaseDOHooks, type DeleteRequest, type DeleteResponse, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, type DocumentDOConfig, type DropIndexRequest, type DropIndexResponse, type DropUniqueConstraintRequest, type DropUniqueConstraintResponse, DurableObjectEngine, type DurableObjectEngineOptions, type DurableObjectId, type DurableObjectNamespace, type DurableObjectState, type DurableObjectStorage, type DurableObjectStub, type Env, type ErrorResponse, type GroupByField$1 as GroupByField, type HookContext, type HookResult, type IncrementRequest, type IncrementResponse, type IndexEntry, type IndexListResponse, JsonQueryTranslator, type JsonQueryTranslatorOptions, JsonSchemaDDL, type JsonSchemaOptions, type PatchRequest, type PatchResponse, type QueryRequest, type QueryResponse, type RegisterIndexRequest, type RegisterIndexResponse, type RegisterUniqueConstraintRequest, type RegisterUniqueConstraintResponse, type SaveRequest, type SaveResponse, type SchemaConstraintInfo, type SchemaFieldInfo, type SchemaModelInfo, type SchemaResponse, type SqlStorage, type SqlStorageCursor, type StringSetMembership$1 as StringSetMembership, type StringSetUpdateRequest, type StringSetUpdateResponse, type SyncIndexesRequest, type SyncIndexesResponse, type UniqueConstraintEntry, type UniqueConstraintListResponse, clearMetaSyncCache, createDatabaseDO, createDocumentDO, discoverModelNames, discoverSchema, handleRequest, inferFieldType, loadSchemaFromTomlString, registerFunctionDefault, schemaToToml, syncInferredMeta, syncModelMeta };
2058
+ /**
2059
+ * Produce a deterministic, SQL-safe alias suffix derived from arbitrary input.
2060
+ * The resulting string always matches our identifier policy when prefixed with
2061
+ * a valid identifier segment.
2062
+ */
2063
+ declare function deterministicIdentifierHash(value: string): string;
2064
+ /**
2065
+ * Build a safe alias using a prefix that already conforms to the identifier
2066
+ * policy along with a deterministic hash of the raw descriptor.
2067
+ */
2068
+ declare function buildSafeAlias(prefix: string, rawDescriptor: string): string;
2069
+
2070
+ export { type AfterQueryContext, type AfterQueryResult, type AggregateRequest, type AggregateResponse, type AggregationOperation$1 as AggregationOperation, type AggregationOptions$1 as AggregationOptions, type AggregationResult$1 as AggregationResult, type BatchOperation, type BatchOperationResult, type BatchRequest, type BatchResponse, type BeforeDeleteContext, type BeforeQueryContext, type BeforeQueryResult, type BeforeSaveContext, type CountRequest, type CountResponse, type DatabaseDOConfig, type DatabaseDOHooks, type DeleteRequest, type DeleteResponse, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, type DocumentDOConfig, type DropIndexRequest, type DropIndexResponse, type DropUniqueConstraintRequest, type DropUniqueConstraintResponse, DurableObjectEngine, type DurableObjectEngineOptions, type DurableObjectId, type DurableObjectNamespace, type DurableObjectState, type DurableObjectStorage, type DurableObjectStub, type Env, type ErrorResponse, type GroupByField$1 as GroupByField, type HookContext, type HookResult, type IncrementRequest, type IncrementResponse, type IndexEntry, type IndexListResponse, JsonQueryTranslator, type JsonQueryTranslatorOptions, JsonSchemaDDL, type JsonSchemaOptions, type PatchRequest, type PatchResponse, type QueryRequest, type QueryResponse, type RegisterIndexRequest, type RegisterIndexResponse, type RegisterUniqueConstraintRequest, type RegisterUniqueConstraintResponse, type SaveRequest, type SaveResponse, type SchemaConstraintInfo, type SchemaFieldInfo, type SchemaModelInfo, type SchemaResponse, type SqlStorage, type SqlStorageCursor, type StringSetMembership$1 as StringSetMembership, type StringSetUpdateRequest, type StringSetUpdateResponse, type SyncIndexesRequest, type SyncIndexesResponse, type UniqueConstraintEntry, type UniqueConstraintListResponse, buildSafeAlias, checkAutoStampsReservedFields, checkReservedFieldName, clearMetaSyncCache, createDatabaseDO, createDocumentDO, deterministicIdentifierHash, discoverModelNames, discoverSchema, handleRequest, inferFieldType, loadSchemaFromTomlString, registerFunctionDefault, schemaToToml, syncInferredMeta, syncModelMeta };
@@ -17,6 +17,21 @@ function assertValidIdentifier(name, context) {
17
17
  function quoteIdentifier(name) {
18
18
  return `"${name.replace(/"/g, '""')}"`;
19
19
  }
20
+ function deterministicIdentifierHash(value) {
21
+ let hash = 0;
22
+ for (let i = 0; i < value.length; i++) {
23
+ hash = (hash << 5) - hash + value.charCodeAt(i);
24
+ hash |= 0;
25
+ }
26
+ const positiveHash = hash === 0 ? 0 : Math.abs(hash);
27
+ return positiveHash.toString(36);
28
+ }
29
+ function buildSafeAlias(prefix, rawDescriptor) {
30
+ const suffix = deterministicIdentifierHash(rawDescriptor);
31
+ const candidate = `${prefix}_${suffix}`;
32
+ assertValidIdentifier(candidate, "Alias generation");
33
+ return candidate;
34
+ }
20
35
  var IDENTIFIER_PATTERN;
21
36
  var init_sql = __esm({
22
37
  "src/utils/sql.ts"() {
@@ -312,6 +327,21 @@ var init_patterns = __esm({
312
327
  }
313
328
  });
314
329
 
330
+ // src/utils/yjsValues.ts
331
+ import * as Y from "yjs";
332
+ function isStringSetYMap(value) {
333
+ if (!(value instanceof Y.Map)) return false;
334
+ for (const v of value.values()) {
335
+ if (v !== true) return false;
336
+ }
337
+ return true;
338
+ }
339
+ var init_yjsValues = __esm({
340
+ "src/utils/yjsValues.ts"() {
341
+ "use strict";
342
+ }
343
+ });
344
+
315
345
  // src/models/StringSet.ts
316
346
  var init_StringSet = __esm({
317
347
  "src/models/StringSet.ts"() {
@@ -338,9 +368,9 @@ var init_DocumentQueryTranslator = __esm({
338
368
  });
339
369
 
340
370
  // src/models/metaSync.ts
341
- import * as Y2 from "yjs";
371
+ import * as Y3 from "yjs";
342
372
  function inferFieldType(value) {
343
- if (value instanceof Y2.Map) return "stringset";
373
+ if (isStringSetYMap(value)) return "stringset";
344
374
  switch (typeof value) {
345
375
  case "string":
346
376
  return "string";
@@ -385,7 +415,7 @@ function syncModelMeta(yDoc, modelName, schema) {
385
415
  if (compoundConstraints.length > 0) {
386
416
  let constraints = meta.get("_constraints");
387
417
  if (!constraints) {
388
- constraints = new Y2.Map();
418
+ constraints = new Y3.Map();
389
419
  meta.set("_constraints", constraints);
390
420
  }
391
421
  for (const constraint of compoundConstraints) {
@@ -396,7 +426,7 @@ function syncModelMeta(yDoc, modelName, schema) {
396
426
  if (relationships && Object.keys(relationships).length > 0) {
397
427
  let rels = meta.get("_relationships");
398
428
  if (!rels) {
399
- rels = new Y2.Map();
429
+ rels = new Y3.Map();
400
430
  meta.set("_relationships", rels);
401
431
  }
402
432
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -408,7 +438,7 @@ function syncModelMeta(yDoc, modelName, schema) {
408
438
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
409
439
  let fieldMeta = metaMap.get(fieldName);
410
440
  if (!fieldMeta) {
411
- fieldMeta = new Y2.Map();
441
+ fieldMeta = new Y3.Map();
412
442
  metaMap.set(fieldName, fieldMeta);
413
443
  }
414
444
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -416,6 +446,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
416
446
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
417
447
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
418
448
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
449
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
419
450
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
420
451
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
421
452
  const encoded = encodeDefault(fieldOpts.default);
@@ -424,7 +455,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
424
455
  function syncConstraintMeta(constraintsMap, constraint) {
425
456
  let cMeta = constraintsMap.get(constraint.name);
426
457
  if (!cMeta) {
427
- cMeta = new Y2.Map();
458
+ cMeta = new Y3.Map();
428
459
  constraintsMap.set(constraint.name, cMeta);
429
460
  }
430
461
  setIfChanged(cMeta, "type", "unique");
@@ -434,7 +465,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
434
465
  function syncRelationshipMeta(relsMap, relName, relConfig) {
435
466
  let relMeta = relsMap.get(relName);
436
467
  if (!relMeta) {
437
- relMeta = new Y2.Map();
468
+ relMeta = new Y3.Map();
438
469
  relsMap.set(relName, relMeta);
439
470
  }
440
471
  for (const [key, value] of Object.entries(relConfig)) {
@@ -449,7 +480,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
449
480
  if (fieldName.startsWith("_")) continue;
450
481
  let fieldMeta = meta.get(fieldName);
451
482
  if (!fieldMeta) {
452
- fieldMeta = new Y2.Map();
483
+ fieldMeta = new Y3.Map();
453
484
  meta.set(fieldName, fieldMeta);
454
485
  }
455
486
  if (!fieldMeta.has("type")) {
@@ -469,13 +500,14 @@ var KNOWN_FUNCTION_DEFAULTS, _syncedCache;
469
500
  var init_metaSync = __esm({
470
501
  "src/models/metaSync.ts"() {
471
502
  "use strict";
503
+ init_yjsValues();
472
504
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
473
505
  _syncedCache = /* @__PURE__ */ new WeakMap();
474
506
  }
475
507
  });
476
508
 
477
509
  // src/models/BaseModel.ts
478
- import * as Y3 from "yjs";
510
+ import * as Y4 from "yjs";
479
511
  import { ulid as ulid2 } from "ulid";
480
512
  function generateULID() {
481
513
  return ulid2();
@@ -489,6 +521,7 @@ var init_BaseModel = __esm({
489
521
  init_CursorManager();
490
522
  init_sql();
491
523
  init_metaSync();
524
+ init_yjsValues();
492
525
  registerFunctionDefault(generateULID, "generate_ulid");
493
526
  }
494
527
  });
@@ -2200,6 +2233,50 @@ var JsonQueryTranslator = class {
2200
2233
  init_CursorManager();
2201
2234
  init_sql();
2202
2235
  import { ulid } from "ulid";
2236
+ function checkReservedFieldName(fieldName) {
2237
+ if (fieldName === "id") return null;
2238
+ if (fieldName === "type") {
2239
+ return `Field 'type' is reserved (maps to internal _type column in queries)`;
2240
+ }
2241
+ if (fieldName.startsWith("_")) {
2242
+ return `Field '${fieldName}' is reserved (fields starting with '_' are internal)`;
2243
+ }
2244
+ return null;
2245
+ }
2246
+ function checkAutoStampsReservedFields(autoStamps) {
2247
+ if (!autoStamps) return null;
2248
+ for (const field of autoStamps.create || []) {
2249
+ const err = checkReservedFieldName(field);
2250
+ if (err) return `autoStamps.create: ${err}`;
2251
+ }
2252
+ for (const field of autoStamps.update || []) {
2253
+ const err = checkReservedFieldName(field);
2254
+ if (err) return `autoStamps.update: ${err}`;
2255
+ }
2256
+ return null;
2257
+ }
2258
+ function applyAutoStamps(data, autoStamps, isNew) {
2259
+ if (!autoStamps || !data) return;
2260
+ const reservedError = checkAutoStampsReservedFields(autoStamps);
2261
+ if (reservedError) {
2262
+ throw new Error(reservedError);
2263
+ }
2264
+ const now = Date.now();
2265
+ if (isNew && autoStamps.create) {
2266
+ for (const field of autoStamps.create) {
2267
+ if (data[field] === void 0 || data[field] === null) {
2268
+ data[field] = now;
2269
+ }
2270
+ }
2271
+ }
2272
+ if (autoStamps.update) {
2273
+ for (const field of autoStamps.update) {
2274
+ if (data[field] === void 0 || data[field] === null) {
2275
+ data[field] = now;
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2203
2280
  function createDatabaseDO(config = {}) {
2204
2281
  const hooks = config.hooks;
2205
2282
  return class DocumentDO {
@@ -2380,7 +2457,15 @@ function createDatabaseDO(config = {}) {
2380
2457
  /** @internal */
2381
2458
  async _handleSave(request, docId) {
2382
2459
  const body = await request.json();
2383
- const { modelName, data, stringSets, ifNotExists, condition, upsertOn } = body;
2460
+ const {
2461
+ modelName,
2462
+ data,
2463
+ stringSets,
2464
+ ifNotExists,
2465
+ condition,
2466
+ upsertOn,
2467
+ autoStamps
2468
+ } = body;
2384
2469
  let id = body.id;
2385
2470
  if (!modelName || !id && !upsertOn) {
2386
2471
  return this._errorResponse("modelName is required, and either id or upsertOn must be provided", 400);
@@ -2394,6 +2479,12 @@ function createDatabaseDO(config = {}) {
2394
2479
  return this._errorResponse(reservedError, 400);
2395
2480
  }
2396
2481
  }
2482
+ {
2483
+ const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
2484
+ if (autoStampsReservedError) {
2485
+ return this._errorResponse(autoStampsReservedError, 400);
2486
+ }
2487
+ }
2397
2488
  if (upsertOn) {
2398
2489
  if (data[upsertOn] === void 0 || data[upsertOn] === null || data[upsertOn] === "") {
2399
2490
  return this._errorResponse(
@@ -2443,8 +2534,9 @@ function createDatabaseDO(config = {}) {
2443
2534
  409
2444
2535
  );
2445
2536
  }
2537
+ const isNew = !this._engine.recordExists(modelName, id);
2538
+ applyAutoStamps(data, autoStamps, isNew);
2446
2539
  if (hooks?.beforeSave) {
2447
- const isNew = !this._engine.recordExists(modelName, id);
2448
2540
  const ctx = {
2449
2541
  modelName,
2450
2542
  docId,
@@ -2493,7 +2585,7 @@ function createDatabaseDO(config = {}) {
2493
2585
  /** @internal — Partial update: merge provided fields into existing record */
2494
2586
  async _handlePatch(request, docId) {
2495
2587
  const body = await request.json();
2496
- const { modelName, id, data, stringSets, condition } = body;
2588
+ const { modelName, id, data, stringSets, condition, autoStamps } = body;
2497
2589
  if (!modelName || !id || !data) {
2498
2590
  return this._errorResponse("modelName, id, and data are required", 400);
2499
2591
  }
@@ -2501,6 +2593,12 @@ function createDatabaseDO(config = {}) {
2501
2593
  if (reservedError) {
2502
2594
  return this._errorResponse(reservedError, 400);
2503
2595
  }
2596
+ {
2597
+ const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
2598
+ if (autoStampsReservedError) {
2599
+ return this._errorResponse(autoStampsReservedError, 400);
2600
+ }
2601
+ }
2504
2602
  if (!this._engine.recordExists(modelName, id)) {
2505
2603
  return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2506
2604
  }
@@ -2510,6 +2608,7 @@ function createDatabaseDO(config = {}) {
2510
2608
  409
2511
2609
  );
2512
2610
  }
2611
+ applyAutoStamps(data, autoStamps, false);
2513
2612
  if (hooks?.beforeSave) {
2514
2613
  const ctx = {
2515
2614
  modelName,
@@ -2596,6 +2695,7 @@ function createDatabaseDO(config = {}) {
2596
2695
  const hookDenials = /* @__PURE__ */ new Map();
2597
2696
  const resolvedBatchIds = /* @__PURE__ */ new Map();
2598
2697
  const opErrors = /* @__PURE__ */ new Map();
2698
+ const batchCreatedIds = /* @__PURE__ */ new Set();
2599
2699
  for (let i = 0; i < operations.length; i++) {
2600
2700
  const op = operations[i];
2601
2701
  if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
@@ -2647,9 +2747,30 @@ function createDatabaseDO(config = {}) {
2647
2747
  400
2648
2748
  );
2649
2749
  }
2750
+ const autoStampsReservedError = checkAutoStampsReservedFields(
2751
+ op.autoStamps
2752
+ );
2753
+ if (autoStampsReservedError) {
2754
+ return this._errorResponse(
2755
+ `Operation ${i}: ${autoStampsReservedError}`,
2756
+ 400
2757
+ );
2758
+ }
2759
+ const effectiveId = resolvedBatchIds.get(i) || op.id || "";
2760
+ const isNew = op.op === "save" ? (
2761
+ // Codex P2 #3: also treat the id as not-new if a previous
2762
+ // op in this batch already resolved to a save for the same
2763
+ // model+id. Without this, two saves to the same fresh id
2764
+ // both compute isNew=true against the pre-transaction
2765
+ // database, so a `'create'` autoStamp on the second op
2766
+ // would overwrite the first op's stamp.
2767
+ !batchCreatedIds.has(`${op.modelName}/${effectiveId}`) && !this._engine.recordExists(op.modelName, effectiveId)
2768
+ ) : false;
2769
+ if (op.op === "save" && isNew && effectiveId) {
2770
+ batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
2771
+ }
2772
+ applyAutoStamps(op.data, op.autoStamps, isNew);
2650
2773
  if (hooks?.beforeSave) {
2651
- const effectiveId = resolvedBatchIds.get(i) || op.id || "";
2652
- const isNew = op.op === "save" ? !this._engine.recordExists(op.modelName, effectiveId) : false;
2653
2774
  const ctx = {
2654
2775
  modelName: op.modelName,
2655
2776
  docId,
@@ -4010,7 +4131,8 @@ async function handleRequest(request, env) {
4010
4131
  }
4011
4132
 
4012
4133
  // src/utils/yDocSchema.ts
4013
- import * as Y from "yjs";
4134
+ init_yjsValues();
4135
+ import * as Y2 from "yjs";
4014
4136
  function discoverSchema(yDoc) {
4015
4137
  const models = {};
4016
4138
  const metaNames = /* @__PURE__ */ new Set();
@@ -4044,7 +4166,7 @@ function discoverModelNames(yDoc) {
4044
4166
  function materializeMap(yDoc, key) {
4045
4167
  try {
4046
4168
  const map = yDoc.getMap(key);
4047
- return map instanceof Y.Map ? map : null;
4169
+ return map instanceof Y2.Map ? map : null;
4048
4170
  } catch {
4049
4171
  return null;
4050
4172
  }
@@ -4054,11 +4176,11 @@ function readModelMeta(metaMap) {
4054
4176
  let constraints;
4055
4177
  let relationships;
4056
4178
  for (const [key, value] of metaMap.entries()) {
4057
- if (key === "_constraints" && value instanceof Y.Map) {
4179
+ if (key === "_constraints" && value instanceof Y2.Map) {
4058
4180
  constraints = readConstraints(value);
4059
- } else if (key === "_relationships" && value instanceof Y.Map) {
4181
+ } else if (key === "_relationships" && value instanceof Y2.Map) {
4060
4182
  relationships = readRelationships(value);
4061
- } else if (value instanceof Y.Map) {
4183
+ } else if (value instanceof Y2.Map) {
4062
4184
  fields[key] = readFieldMeta(value);
4063
4185
  }
4064
4186
  }
@@ -4077,6 +4199,10 @@ function readFieldMeta(fieldMap) {
4077
4199
  if (fieldMap.get("unique") === true) field.unique = true;
4078
4200
  if (fieldMap.get("required") === true) field.required = true;
4079
4201
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
4202
+ const autoStamp = fieldMap.get("autoStamp");
4203
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
4204
+ field.autoStamp = autoStamp;
4205
+ }
4080
4206
  const def = fieldMap.get("default");
4081
4207
  if (def !== void 0) field.default = def;
4082
4208
  const maxLength = fieldMap.get("maxLength");
@@ -4089,7 +4215,7 @@ function inferModelFromData(dataMap) {
4089
4215
  const fields = {};
4090
4216
  let sampled = 0;
4091
4217
  for (const [_recordId, recordValue] of dataMap.entries()) {
4092
- if (!(recordValue instanceof Y.Map)) continue;
4218
+ if (!(recordValue instanceof Y2.Map)) continue;
4093
4219
  if (++sampled > 5) break;
4094
4220
  for (const [fieldName, value] of recordValue.entries()) {
4095
4221
  if (fieldName.startsWith("_")) continue;
@@ -4102,7 +4228,7 @@ function inferModelFromData(dataMap) {
4102
4228
  return { fields };
4103
4229
  }
4104
4230
  function inferTypeFromValue(value) {
4105
- if (value instanceof Y.Map) return "stringset";
4231
+ if (isStringSetYMap(value)) return "stringset";
4106
4232
  switch (typeof value) {
4107
4233
  case "string":
4108
4234
  return "string";
@@ -4117,7 +4243,7 @@ function inferTypeFromValue(value) {
4117
4243
  function readConstraints(constraintsMap) {
4118
4244
  const out = {};
4119
4245
  for (const [name, value] of constraintsMap.entries()) {
4120
- if (!(value instanceof Y.Map)) continue;
4246
+ if (!(value instanceof Y2.Map)) continue;
4121
4247
  let fields = [];
4122
4248
  const rawFields = value.get("fields");
4123
4249
  if (typeof rawFields === "string") {
@@ -4136,7 +4262,7 @@ function readConstraints(constraintsMap) {
4136
4262
  function readRelationships(relsMap) {
4137
4263
  const out = {};
4138
4264
  for (const [name, value] of relsMap.entries()) {
4139
- if (!(value instanceof Y.Map)) continue;
4265
+ if (!(value instanceof Y2.Map)) continue;
4140
4266
  const rel = {};
4141
4267
  for (const [k, v] of value.entries()) {
4142
4268
  rel[k] = v;
@@ -4147,6 +4273,7 @@ function readRelationships(relsMap) {
4147
4273
  }
4148
4274
  var CAMEL_TO_SNAKE = {
4149
4275
  autoAssign: "auto_assign",
4276
+ autoStamp: "auto_stamp",
4150
4277
  maxLength: "max_length",
4151
4278
  maxCount: "max_count",
4152
4279
  relatedIdField: "related_id_field",
@@ -4177,6 +4304,7 @@ function schemaToToml(schema) {
4177
4304
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
4178
4305
  lines.push(`type = ${tomlValue(field.type)}`);
4179
4306
  if (field.autoAssign) lines.push("auto_assign = true");
4307
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
4180
4308
  if (field.indexed) lines.push("indexed = true");
4181
4309
  if (field.unique) lines.push("unique = true");
4182
4310
  if (field.required) lines.push("required = true");
@@ -4300,10 +4428,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
4300
4428
  "unique",
4301
4429
  "required",
4302
4430
  "auto_assign",
4431
+ "auto_stamp",
4303
4432
  "max_length",
4304
4433
  "max_count",
4305
- "default"
4434
+ "default",
4435
+ "enum"
4306
4436
  ]);
4437
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
4307
4438
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
4308
4439
  "fields",
4309
4440
  "relationships",
@@ -4345,9 +4476,37 @@ function parseFieldOptions(raw, context, strict) {
4345
4476
  if (raw.unique === true) opts.unique = true;
4346
4477
  if (raw.required === true) opts.required = true;
4347
4478
  if (raw.auto_assign === true) opts.autoAssign = true;
4479
+ if (raw.auto_stamp !== void 0) {
4480
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
4481
+ throw new Error(
4482
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
4483
+ ...VALID_AUTO_STAMP_VALUES
4484
+ ].join(", ")}`
4485
+ );
4486
+ }
4487
+ opts.autoStamp = raw.auto_stamp;
4488
+ }
4348
4489
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
4349
4490
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
4350
4491
  if (raw.default !== void 0) opts.default = raw.default;
4492
+ if (raw.enum !== void 0) {
4493
+ if (raw.type !== "string") {
4494
+ throw new Error(
4495
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
4496
+ );
4497
+ }
4498
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
4499
+ throw new Error(
4500
+ `${context}: \`enum\` must be a non-empty array of strings.`
4501
+ );
4502
+ }
4503
+ if (!raw.enum.every((v) => typeof v === "string")) {
4504
+ throw new Error(
4505
+ `${context}: \`enum\` values must all be strings.`
4506
+ );
4507
+ }
4508
+ opts.enum = [...raw.enum];
4509
+ }
4351
4510
  return opts;
4352
4511
  }
4353
4512
  function requireField(raw, field, context) {
@@ -4471,13 +4630,18 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
4471
4630
 
4472
4631
  // src/cloudflare-do.ts
4473
4632
  init_metaSync();
4633
+ init_sql();
4474
4634
  export {
4475
4635
  DurableObjectEngine,
4476
4636
  JsonQueryTranslator,
4477
4637
  JsonSchemaDDL,
4638
+ buildSafeAlias,
4639
+ checkAutoStampsReservedFields,
4640
+ checkReservedFieldName,
4478
4641
  clearMetaSyncCache,
4479
4642
  createDatabaseDO,
4480
4643
  createDocumentDO,
4644
+ deterministicIdentifierHash,
4481
4645
  discoverModelNames,
4482
4646
  discoverSchema,
4483
4647
  handleRequest,