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.
@@ -44,6 +44,21 @@ function assertValidIdentifier(name, context) {
44
44
  function quoteIdentifier(name) {
45
45
  return `"${name.replace(/"/g, '""')}"`;
46
46
  }
47
+ function deterministicIdentifierHash(value) {
48
+ let hash = 0;
49
+ for (let i = 0; i < value.length; i++) {
50
+ hash = (hash << 5) - hash + value.charCodeAt(i);
51
+ hash |= 0;
52
+ }
53
+ const positiveHash = hash === 0 ? 0 : Math.abs(hash);
54
+ return positiveHash.toString(36);
55
+ }
56
+ function buildSafeAlias(prefix, rawDescriptor) {
57
+ const suffix = deterministicIdentifierHash(rawDescriptor);
58
+ const candidate = `${prefix}_${suffix}`;
59
+ assertValidIdentifier(candidate, "Alias generation");
60
+ return candidate;
61
+ }
47
62
  var IDENTIFIER_PATTERN;
48
63
  var init_sql = __esm({
49
64
  "src/utils/sql.ts"() {
@@ -339,6 +354,22 @@ var init_patterns = __esm({
339
354
  }
340
355
  });
341
356
 
357
+ // src/utils/yjsValues.ts
358
+ function isStringSetYMap(value) {
359
+ if (!(value instanceof Y.Map)) return false;
360
+ for (const v of value.values()) {
361
+ if (v !== true) return false;
362
+ }
363
+ return true;
364
+ }
365
+ var Y;
366
+ var init_yjsValues = __esm({
367
+ "src/utils/yjsValues.ts"() {
368
+ "use strict";
369
+ Y = __toESM(require("yjs"), 1);
370
+ }
371
+ });
372
+
342
373
  // src/models/StringSet.ts
343
374
  var init_StringSet = __esm({
344
375
  "src/models/StringSet.ts"() {
@@ -366,7 +397,7 @@ var init_DocumentQueryTranslator = __esm({
366
397
 
367
398
  // src/models/metaSync.ts
368
399
  function inferFieldType(value) {
369
- if (value instanceof Y2.Map) return "stringset";
400
+ if (isStringSetYMap(value)) return "stringset";
370
401
  switch (typeof value) {
371
402
  case "string":
372
403
  return "string";
@@ -411,7 +442,7 @@ function syncModelMeta(yDoc, modelName, schema) {
411
442
  if (compoundConstraints.length > 0) {
412
443
  let constraints = meta.get("_constraints");
413
444
  if (!constraints) {
414
- constraints = new Y2.Map();
445
+ constraints = new Y3.Map();
415
446
  meta.set("_constraints", constraints);
416
447
  }
417
448
  for (const constraint of compoundConstraints) {
@@ -422,7 +453,7 @@ function syncModelMeta(yDoc, modelName, schema) {
422
453
  if (relationships && Object.keys(relationships).length > 0) {
423
454
  let rels = meta.get("_relationships");
424
455
  if (!rels) {
425
- rels = new Y2.Map();
456
+ rels = new Y3.Map();
426
457
  meta.set("_relationships", rels);
427
458
  }
428
459
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -434,7 +465,7 @@ function syncModelMeta(yDoc, modelName, schema) {
434
465
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
435
466
  let fieldMeta = metaMap.get(fieldName);
436
467
  if (!fieldMeta) {
437
- fieldMeta = new Y2.Map();
468
+ fieldMeta = new Y3.Map();
438
469
  metaMap.set(fieldName, fieldMeta);
439
470
  }
440
471
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -442,6 +473,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
442
473
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
443
474
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
444
475
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
476
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
445
477
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
446
478
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
447
479
  const encoded = encodeDefault(fieldOpts.default);
@@ -450,7 +482,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
450
482
  function syncConstraintMeta(constraintsMap, constraint) {
451
483
  let cMeta = constraintsMap.get(constraint.name);
452
484
  if (!cMeta) {
453
- cMeta = new Y2.Map();
485
+ cMeta = new Y3.Map();
454
486
  constraintsMap.set(constraint.name, cMeta);
455
487
  }
456
488
  setIfChanged(cMeta, "type", "unique");
@@ -460,7 +492,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
460
492
  function syncRelationshipMeta(relsMap, relName, relConfig) {
461
493
  let relMeta = relsMap.get(relName);
462
494
  if (!relMeta) {
463
- relMeta = new Y2.Map();
495
+ relMeta = new Y3.Map();
464
496
  relsMap.set(relName, relMeta);
465
497
  }
466
498
  for (const [key, value] of Object.entries(relConfig)) {
@@ -475,7 +507,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
475
507
  if (fieldName.startsWith("_")) continue;
476
508
  let fieldMeta = meta.get(fieldName);
477
509
  if (!fieldMeta) {
478
- fieldMeta = new Y2.Map();
510
+ fieldMeta = new Y3.Map();
479
511
  meta.set(fieldName, fieldMeta);
480
512
  }
481
513
  if (!fieldMeta.has("type")) {
@@ -491,11 +523,12 @@ function setIfChanged(map, key, value) {
491
523
  map.set(key, value);
492
524
  }
493
525
  }
494
- var Y2, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
526
+ var Y3, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
495
527
  var init_metaSync = __esm({
496
528
  "src/models/metaSync.ts"() {
497
529
  "use strict";
498
- Y2 = __toESM(require("yjs"), 1);
530
+ Y3 = __toESM(require("yjs"), 1);
531
+ init_yjsValues();
499
532
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
500
533
  _syncedCache = /* @__PURE__ */ new WeakMap();
501
534
  }
@@ -505,11 +538,11 @@ var init_metaSync = __esm({
505
538
  function generateULID() {
506
539
  return (0, import_ulid2.ulid)();
507
540
  }
508
- var Y3, import_ulid2;
541
+ var Y4, import_ulid2;
509
542
  var init_BaseModel = __esm({
510
543
  "src/models/BaseModel.ts"() {
511
544
  "use strict";
512
- Y3 = __toESM(require("yjs"), 1);
545
+ Y4 = __toESM(require("yjs"), 1);
513
546
  import_ulid2 = require("ulid");
514
547
  init_StringSet();
515
548
  init_documentTypes();
@@ -517,6 +550,7 @@ var init_BaseModel = __esm({
517
550
  init_CursorManager();
518
551
  init_sql();
519
552
  init_metaSync();
553
+ init_yjsValues();
520
554
  registerFunctionDefault(generateULID, "generate_ulid");
521
555
  }
522
556
  });
@@ -544,9 +578,13 @@ __export(cloudflare_do_exports, {
544
578
  DurableObjectEngine: () => DurableObjectEngine,
545
579
  JsonQueryTranslator: () => JsonQueryTranslator,
546
580
  JsonSchemaDDL: () => JsonSchemaDDL,
581
+ buildSafeAlias: () => buildSafeAlias,
582
+ checkAutoStampsReservedFields: () => checkAutoStampsReservedFields,
583
+ checkReservedFieldName: () => checkReservedFieldName,
547
584
  clearMetaSyncCache: () => clearMetaSyncCache,
548
585
  createDatabaseDO: () => createDatabaseDO,
549
586
  createDocumentDO: () => createDocumentDO,
587
+ deterministicIdentifierHash: () => deterministicIdentifierHash,
550
588
  discoverModelNames: () => discoverModelNames,
551
589
  discoverSchema: () => discoverSchema,
552
590
  handleRequest: () => handleRequest,
@@ -2249,6 +2287,50 @@ var JsonQueryTranslator = class {
2249
2287
  init_CursorManager();
2250
2288
  init_sql();
2251
2289
  var import_ulid = require("ulid");
2290
+ function checkReservedFieldName(fieldName) {
2291
+ if (fieldName === "id") return null;
2292
+ if (fieldName === "type") {
2293
+ return `Field 'type' is reserved (maps to internal _type column in queries)`;
2294
+ }
2295
+ if (fieldName.startsWith("_")) {
2296
+ return `Field '${fieldName}' is reserved (fields starting with '_' are internal)`;
2297
+ }
2298
+ return null;
2299
+ }
2300
+ function checkAutoStampsReservedFields(autoStamps) {
2301
+ if (!autoStamps) return null;
2302
+ for (const field of autoStamps.create || []) {
2303
+ const err = checkReservedFieldName(field);
2304
+ if (err) return `autoStamps.create: ${err}`;
2305
+ }
2306
+ for (const field of autoStamps.update || []) {
2307
+ const err = checkReservedFieldName(field);
2308
+ if (err) return `autoStamps.update: ${err}`;
2309
+ }
2310
+ return null;
2311
+ }
2312
+ function applyAutoStamps(data, autoStamps, isNew) {
2313
+ if (!autoStamps || !data) return;
2314
+ const reservedError = checkAutoStampsReservedFields(autoStamps);
2315
+ if (reservedError) {
2316
+ throw new Error(reservedError);
2317
+ }
2318
+ const now = Date.now();
2319
+ if (isNew && autoStamps.create) {
2320
+ for (const field of autoStamps.create) {
2321
+ if (data[field] === void 0 || data[field] === null) {
2322
+ data[field] = now;
2323
+ }
2324
+ }
2325
+ }
2326
+ if (autoStamps.update) {
2327
+ for (const field of autoStamps.update) {
2328
+ if (data[field] === void 0 || data[field] === null) {
2329
+ data[field] = now;
2330
+ }
2331
+ }
2332
+ }
2333
+ }
2252
2334
  function createDatabaseDO(config = {}) {
2253
2335
  const hooks = config.hooks;
2254
2336
  return class DocumentDO {
@@ -2429,7 +2511,15 @@ function createDatabaseDO(config = {}) {
2429
2511
  /** @internal */
2430
2512
  async _handleSave(request, docId) {
2431
2513
  const body = await request.json();
2432
- const { modelName, data, stringSets, ifNotExists, condition, upsertOn } = body;
2514
+ const {
2515
+ modelName,
2516
+ data,
2517
+ stringSets,
2518
+ ifNotExists,
2519
+ condition,
2520
+ upsertOn,
2521
+ autoStamps
2522
+ } = body;
2433
2523
  let id = body.id;
2434
2524
  if (!modelName || !id && !upsertOn) {
2435
2525
  return this._errorResponse("modelName is required, and either id or upsertOn must be provided", 400);
@@ -2443,6 +2533,12 @@ function createDatabaseDO(config = {}) {
2443
2533
  return this._errorResponse(reservedError, 400);
2444
2534
  }
2445
2535
  }
2536
+ {
2537
+ const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
2538
+ if (autoStampsReservedError) {
2539
+ return this._errorResponse(autoStampsReservedError, 400);
2540
+ }
2541
+ }
2446
2542
  if (upsertOn) {
2447
2543
  if (data[upsertOn] === void 0 || data[upsertOn] === null || data[upsertOn] === "") {
2448
2544
  return this._errorResponse(
@@ -2492,8 +2588,9 @@ function createDatabaseDO(config = {}) {
2492
2588
  409
2493
2589
  );
2494
2590
  }
2591
+ const isNew = !this._engine.recordExists(modelName, id);
2592
+ applyAutoStamps(data, autoStamps, isNew);
2495
2593
  if (hooks?.beforeSave) {
2496
- const isNew = !this._engine.recordExists(modelName, id);
2497
2594
  const ctx = {
2498
2595
  modelName,
2499
2596
  docId,
@@ -2542,7 +2639,7 @@ function createDatabaseDO(config = {}) {
2542
2639
  /** @internal — Partial update: merge provided fields into existing record */
2543
2640
  async _handlePatch(request, docId) {
2544
2641
  const body = await request.json();
2545
- const { modelName, id, data, stringSets, condition } = body;
2642
+ const { modelName, id, data, stringSets, condition, autoStamps } = body;
2546
2643
  if (!modelName || !id || !data) {
2547
2644
  return this._errorResponse("modelName, id, and data are required", 400);
2548
2645
  }
@@ -2550,6 +2647,12 @@ function createDatabaseDO(config = {}) {
2550
2647
  if (reservedError) {
2551
2648
  return this._errorResponse(reservedError, 400);
2552
2649
  }
2650
+ {
2651
+ const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
2652
+ if (autoStampsReservedError) {
2653
+ return this._errorResponse(autoStampsReservedError, 400);
2654
+ }
2655
+ }
2553
2656
  if (!this._engine.recordExists(modelName, id)) {
2554
2657
  return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2555
2658
  }
@@ -2559,6 +2662,7 @@ function createDatabaseDO(config = {}) {
2559
2662
  409
2560
2663
  );
2561
2664
  }
2665
+ applyAutoStamps(data, autoStamps, false);
2562
2666
  if (hooks?.beforeSave) {
2563
2667
  const ctx = {
2564
2668
  modelName,
@@ -2645,6 +2749,7 @@ function createDatabaseDO(config = {}) {
2645
2749
  const hookDenials = /* @__PURE__ */ new Map();
2646
2750
  const resolvedBatchIds = /* @__PURE__ */ new Map();
2647
2751
  const opErrors = /* @__PURE__ */ new Map();
2752
+ const batchCreatedIds = /* @__PURE__ */ new Set();
2648
2753
  for (let i = 0; i < operations.length; i++) {
2649
2754
  const op = operations[i];
2650
2755
  if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
@@ -2696,9 +2801,30 @@ function createDatabaseDO(config = {}) {
2696
2801
  400
2697
2802
  );
2698
2803
  }
2804
+ const autoStampsReservedError = checkAutoStampsReservedFields(
2805
+ op.autoStamps
2806
+ );
2807
+ if (autoStampsReservedError) {
2808
+ return this._errorResponse(
2809
+ `Operation ${i}: ${autoStampsReservedError}`,
2810
+ 400
2811
+ );
2812
+ }
2813
+ const effectiveId = resolvedBatchIds.get(i) || op.id || "";
2814
+ const isNew = op.op === "save" ? (
2815
+ // Codex P2 #3: also treat the id as not-new if a previous
2816
+ // op in this batch already resolved to a save for the same
2817
+ // model+id. Without this, two saves to the same fresh id
2818
+ // both compute isNew=true against the pre-transaction
2819
+ // database, so a `'create'` autoStamp on the second op
2820
+ // would overwrite the first op's stamp.
2821
+ !batchCreatedIds.has(`${op.modelName}/${effectiveId}`) && !this._engine.recordExists(op.modelName, effectiveId)
2822
+ ) : false;
2823
+ if (op.op === "save" && isNew && effectiveId) {
2824
+ batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
2825
+ }
2826
+ applyAutoStamps(op.data, op.autoStamps, isNew);
2699
2827
  if (hooks?.beforeSave) {
2700
- const effectiveId = resolvedBatchIds.get(i) || op.id || "";
2701
- const isNew = op.op === "save" ? !this._engine.recordExists(op.modelName, effectiveId) : false;
2702
2828
  const ctx = {
2703
2829
  modelName: op.modelName,
2704
2830
  docId,
@@ -4059,7 +4185,8 @@ async function handleRequest(request, env) {
4059
4185
  }
4060
4186
 
4061
4187
  // src/utils/yDocSchema.ts
4062
- var Y = __toESM(require("yjs"), 1);
4188
+ var Y2 = __toESM(require("yjs"), 1);
4189
+ init_yjsValues();
4063
4190
  function discoverSchema(yDoc) {
4064
4191
  const models = {};
4065
4192
  const metaNames = /* @__PURE__ */ new Set();
@@ -4093,7 +4220,7 @@ function discoverModelNames(yDoc) {
4093
4220
  function materializeMap(yDoc, key) {
4094
4221
  try {
4095
4222
  const map = yDoc.getMap(key);
4096
- return map instanceof Y.Map ? map : null;
4223
+ return map instanceof Y2.Map ? map : null;
4097
4224
  } catch {
4098
4225
  return null;
4099
4226
  }
@@ -4103,11 +4230,11 @@ function readModelMeta(metaMap) {
4103
4230
  let constraints;
4104
4231
  let relationships;
4105
4232
  for (const [key, value] of metaMap.entries()) {
4106
- if (key === "_constraints" && value instanceof Y.Map) {
4233
+ if (key === "_constraints" && value instanceof Y2.Map) {
4107
4234
  constraints = readConstraints(value);
4108
- } else if (key === "_relationships" && value instanceof Y.Map) {
4235
+ } else if (key === "_relationships" && value instanceof Y2.Map) {
4109
4236
  relationships = readRelationships(value);
4110
- } else if (value instanceof Y.Map) {
4237
+ } else if (value instanceof Y2.Map) {
4111
4238
  fields[key] = readFieldMeta(value);
4112
4239
  }
4113
4240
  }
@@ -4126,6 +4253,10 @@ function readFieldMeta(fieldMap) {
4126
4253
  if (fieldMap.get("unique") === true) field.unique = true;
4127
4254
  if (fieldMap.get("required") === true) field.required = true;
4128
4255
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
4256
+ const autoStamp = fieldMap.get("autoStamp");
4257
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
4258
+ field.autoStamp = autoStamp;
4259
+ }
4129
4260
  const def = fieldMap.get("default");
4130
4261
  if (def !== void 0) field.default = def;
4131
4262
  const maxLength = fieldMap.get("maxLength");
@@ -4138,7 +4269,7 @@ function inferModelFromData(dataMap) {
4138
4269
  const fields = {};
4139
4270
  let sampled = 0;
4140
4271
  for (const [_recordId, recordValue] of dataMap.entries()) {
4141
- if (!(recordValue instanceof Y.Map)) continue;
4272
+ if (!(recordValue instanceof Y2.Map)) continue;
4142
4273
  if (++sampled > 5) break;
4143
4274
  for (const [fieldName, value] of recordValue.entries()) {
4144
4275
  if (fieldName.startsWith("_")) continue;
@@ -4151,7 +4282,7 @@ function inferModelFromData(dataMap) {
4151
4282
  return { fields };
4152
4283
  }
4153
4284
  function inferTypeFromValue(value) {
4154
- if (value instanceof Y.Map) return "stringset";
4285
+ if (isStringSetYMap(value)) return "stringset";
4155
4286
  switch (typeof value) {
4156
4287
  case "string":
4157
4288
  return "string";
@@ -4166,7 +4297,7 @@ function inferTypeFromValue(value) {
4166
4297
  function readConstraints(constraintsMap) {
4167
4298
  const out = {};
4168
4299
  for (const [name, value] of constraintsMap.entries()) {
4169
- if (!(value instanceof Y.Map)) continue;
4300
+ if (!(value instanceof Y2.Map)) continue;
4170
4301
  let fields = [];
4171
4302
  const rawFields = value.get("fields");
4172
4303
  if (typeof rawFields === "string") {
@@ -4185,7 +4316,7 @@ function readConstraints(constraintsMap) {
4185
4316
  function readRelationships(relsMap) {
4186
4317
  const out = {};
4187
4318
  for (const [name, value] of relsMap.entries()) {
4188
- if (!(value instanceof Y.Map)) continue;
4319
+ if (!(value instanceof Y2.Map)) continue;
4189
4320
  const rel = {};
4190
4321
  for (const [k, v] of value.entries()) {
4191
4322
  rel[k] = v;
@@ -4196,6 +4327,7 @@ function readRelationships(relsMap) {
4196
4327
  }
4197
4328
  var CAMEL_TO_SNAKE = {
4198
4329
  autoAssign: "auto_assign",
4330
+ autoStamp: "auto_stamp",
4199
4331
  maxLength: "max_length",
4200
4332
  maxCount: "max_count",
4201
4333
  relatedIdField: "related_id_field",
@@ -4226,6 +4358,7 @@ function schemaToToml(schema) {
4226
4358
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
4227
4359
  lines.push(`type = ${tomlValue(field.type)}`);
4228
4360
  if (field.autoAssign) lines.push("auto_assign = true");
4361
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
4229
4362
  if (field.indexed) lines.push("indexed = true");
4230
4363
  if (field.unique) lines.push("unique = true");
4231
4364
  if (field.required) lines.push("required = true");
@@ -4349,10 +4482,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
4349
4482
  "unique",
4350
4483
  "required",
4351
4484
  "auto_assign",
4485
+ "auto_stamp",
4352
4486
  "max_length",
4353
4487
  "max_count",
4354
- "default"
4488
+ "default",
4489
+ "enum"
4355
4490
  ]);
4491
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
4356
4492
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
4357
4493
  "fields",
4358
4494
  "relationships",
@@ -4394,9 +4530,37 @@ function parseFieldOptions(raw, context, strict) {
4394
4530
  if (raw.unique === true) opts.unique = true;
4395
4531
  if (raw.required === true) opts.required = true;
4396
4532
  if (raw.auto_assign === true) opts.autoAssign = true;
4533
+ if (raw.auto_stamp !== void 0) {
4534
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
4535
+ throw new Error(
4536
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
4537
+ ...VALID_AUTO_STAMP_VALUES
4538
+ ].join(", ")}`
4539
+ );
4540
+ }
4541
+ opts.autoStamp = raw.auto_stamp;
4542
+ }
4397
4543
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
4398
4544
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
4399
4545
  if (raw.default !== void 0) opts.default = raw.default;
4546
+ if (raw.enum !== void 0) {
4547
+ if (raw.type !== "string") {
4548
+ throw new Error(
4549
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
4550
+ );
4551
+ }
4552
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
4553
+ throw new Error(
4554
+ `${context}: \`enum\` must be a non-empty array of strings.`
4555
+ );
4556
+ }
4557
+ if (!raw.enum.every((v) => typeof v === "string")) {
4558
+ throw new Error(
4559
+ `${context}: \`enum\` values must all be strings.`
4560
+ );
4561
+ }
4562
+ opts.enum = [...raw.enum];
4563
+ }
4400
4564
  return opts;
4401
4565
  }
4402
4566
  function requireField(raw, field, context) {
@@ -4520,3 +4684,4 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
4520
4684
 
4521
4685
  // src/cloudflare-do.ts
4522
4686
  init_metaSync();
4687
+ init_sql();
@@ -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 };