js-bao 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"() {
@@ -214,21 +229,36 @@ var init_CursorManager = __esm({
214
229
  return fields;
215
230
  }
216
231
  /**
217
- * Build ORDER BY clause from sort specification
232
+ * Build ORDER BY clause from sort specification.
233
+ *
234
+ * `direction` is the pagination direction (1 = forward, -1 = backward).
235
+ * For backward pagination each field's effective ORDER BY direction is
236
+ * reversed so the cursor's flipped inequality (built by
237
+ * `buildPaginationConditions`) returns the page adjacent to the cursor,
238
+ * not the globally smallest/largest window. Without this,
239
+ * `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
240
+ * rows overall instead of the n rows just below the cursor.
241
+ *
242
+ * The returned `directions` array carries the *declared* per-field sort
243
+ * directions (unaffected by `direction`) — `buildPaginationConditions`
244
+ * needs the declared directions to pick each field's comparison operator.
218
245
  */
219
- static buildOrderClause(sort, fieldFormatter = (field) => field) {
246
+ static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
220
247
  const fields = this.extractSortFields(sort);
221
248
  const clauses = [];
222
249
  const directions = [];
250
+ const sqlDir = (declared) => {
251
+ const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
252
+ return effective === 1 ? "ASC" : "DESC";
253
+ };
223
254
  if (sort && Object.keys(sort).length > 0) {
224
255
  for (const [field, dir] of Object.entries(sort)) {
225
- const sqlDirection = dir === 1 ? "ASC" : "DESC";
226
- clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
256
+ clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
227
257
  directions.push(dir);
228
258
  }
229
259
  }
230
260
  if (!sort || !sort.hasOwnProperty("id")) {
231
- clauses.push(`${fieldFormatter("id")} ASC`);
261
+ clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
232
262
  directions.push(1);
233
263
  } else if (sort && sort.hasOwnProperty("id")) {
234
264
  directions.push(sort["id"]);
@@ -251,18 +281,27 @@ var init_CursorManager = __esm({
251
281
  /**
252
282
  * Generate next and previous cursors from result set
253
283
  */
254
- static generateResultCursors(results, sortFields, _requestedDirection, hasMore, isFirstPage = false) {
284
+ static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
255
285
  if (results.length === 0) {
256
286
  return {};
257
287
  }
258
288
  const cursors = {};
259
289
  if (hasMore && results.length > 0) {
260
290
  const lastResult = results[results.length - 1];
261
- cursors.nextCursor = this.generateCursor(lastResult, sortFields, 1);
291
+ cursors.nextCursor = this.generateCursor(
292
+ lastResult,
293
+ sortFields,
294
+ requestedDirection
295
+ );
262
296
  }
263
297
  if (results.length > 0 && !isFirstPage) {
264
298
  const firstResult = results[0];
265
- cursors.prevCursor = this.generateCursor(firstResult, sortFields, -1);
299
+ const opposite = requestedDirection === 1 ? -1 : 1;
300
+ cursors.prevCursor = this.generateCursor(
301
+ firstResult,
302
+ sortFields,
303
+ opposite
304
+ );
266
305
  }
267
306
  return cursors;
268
307
  }
@@ -312,6 +351,21 @@ var init_patterns = __esm({
312
351
  }
313
352
  });
314
353
 
354
+ // src/utils/yjsValues.ts
355
+ import * as Y from "yjs";
356
+ function isStringSetYMap(value) {
357
+ if (!(value instanceof Y.Map)) return false;
358
+ for (const v of value.values()) {
359
+ if (v !== true) return false;
360
+ }
361
+ return true;
362
+ }
363
+ var init_yjsValues = __esm({
364
+ "src/utils/yjsValues.ts"() {
365
+ "use strict";
366
+ }
367
+ });
368
+
315
369
  // src/models/StringSet.ts
316
370
  var init_StringSet = __esm({
317
371
  "src/models/StringSet.ts"() {
@@ -337,10 +391,35 @@ var init_DocumentQueryTranslator = __esm({
337
391
  }
338
392
  });
339
393
 
394
+ // src/models/writeLifecycle.ts
395
+ import * as Y3 from "yjs";
396
+ import { ulid as ulid2 } from "ulid";
397
+ function registerFunctionDefaultGenerator(name, fn) {
398
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
399
+ }
400
+ var KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
401
+ var init_writeLifecycle = __esm({
402
+ "src/models/writeLifecycle.ts"() {
403
+ "use strict";
404
+ KNOWN_FIELD_TYPE_MAP = {
405
+ string: true,
406
+ number: true,
407
+ boolean: true,
408
+ date: true,
409
+ id: true,
410
+ stringset: true
411
+ };
412
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
413
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
414
+ registerFunctionDefaultGenerator("generate_ulid", () => ulid2());
415
+ UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
416
+ }
417
+ });
418
+
340
419
  // src/models/metaSync.ts
341
- import * as Y2 from "yjs";
420
+ import * as Y4 from "yjs";
342
421
  function inferFieldType(value) {
343
- if (value instanceof Y2.Map) return "stringset";
422
+ if (isStringSetYMap(value)) return "stringset";
344
423
  switch (typeof value) {
345
424
  case "string":
346
425
  return "string";
@@ -359,7 +438,10 @@ function encodeDefault(value) {
359
438
  if (value === void 0 || value === null) return void 0;
360
439
  if (typeof value === "function") {
361
440
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
362
- return name ? `$${name}` : "$unknown_function";
441
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
442
+ }
443
+ if (typeof value === "string" && value.startsWith("$")) {
444
+ return `$${value}`;
363
445
  }
364
446
  return value;
365
447
  }
@@ -385,7 +467,7 @@ function syncModelMeta(yDoc, modelName, schema) {
385
467
  if (compoundConstraints.length > 0) {
386
468
  let constraints = meta.get("_constraints");
387
469
  if (!constraints) {
388
- constraints = new Y2.Map();
470
+ constraints = new Y4.Map();
389
471
  meta.set("_constraints", constraints);
390
472
  }
391
473
  for (const constraint of compoundConstraints) {
@@ -396,7 +478,7 @@ function syncModelMeta(yDoc, modelName, schema) {
396
478
  if (relationships && Object.keys(relationships).length > 0) {
397
479
  let rels = meta.get("_relationships");
398
480
  if (!rels) {
399
- rels = new Y2.Map();
481
+ rels = new Y4.Map();
400
482
  meta.set("_relationships", rels);
401
483
  }
402
484
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -408,7 +490,7 @@ function syncModelMeta(yDoc, modelName, schema) {
408
490
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
409
491
  let fieldMeta = metaMap.get(fieldName);
410
492
  if (!fieldMeta) {
411
- fieldMeta = new Y2.Map();
493
+ fieldMeta = new Y4.Map();
412
494
  metaMap.set(fieldName, fieldMeta);
413
495
  }
414
496
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -416,6 +498,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
416
498
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
417
499
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
418
500
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
501
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
419
502
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
420
503
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
421
504
  const encoded = encodeDefault(fieldOpts.default);
@@ -424,7 +507,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
424
507
  function syncConstraintMeta(constraintsMap, constraint) {
425
508
  let cMeta = constraintsMap.get(constraint.name);
426
509
  if (!cMeta) {
427
- cMeta = new Y2.Map();
510
+ cMeta = new Y4.Map();
428
511
  constraintsMap.set(constraint.name, cMeta);
429
512
  }
430
513
  setIfChanged(cMeta, "type", "unique");
@@ -434,7 +517,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
434
517
  function syncRelationshipMeta(relsMap, relName, relConfig) {
435
518
  let relMeta = relsMap.get(relName);
436
519
  if (!relMeta) {
437
- relMeta = new Y2.Map();
520
+ relMeta = new Y4.Map();
438
521
  relsMap.set(relName, relMeta);
439
522
  }
440
523
  for (const [key, value] of Object.entries(relConfig)) {
@@ -449,7 +532,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
449
532
  if (fieldName.startsWith("_")) continue;
450
533
  let fieldMeta = meta.get(fieldName);
451
534
  if (!fieldMeta) {
452
- fieldMeta = new Y2.Map();
535
+ fieldMeta = new Y4.Map();
453
536
  meta.set(fieldName, fieldMeta);
454
537
  }
455
538
  if (!fieldMeta.has("type")) {
@@ -469,16 +552,29 @@ var KNOWN_FUNCTION_DEFAULTS, _syncedCache;
469
552
  var init_metaSync = __esm({
470
553
  "src/models/metaSync.ts"() {
471
554
  "use strict";
555
+ init_yjsValues();
556
+ init_writeLifecycle();
472
557
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
473
558
  _syncedCache = /* @__PURE__ */ new WeakMap();
474
559
  }
475
560
  });
476
561
 
562
+ // src/utils/uniqueIndex.ts
563
+ import * as Y5 from "yjs";
564
+ function fieldUniqueConstraintName(modelName, fieldName) {
565
+ return `${modelName}_${fieldName}_unique`;
566
+ }
567
+ var init_uniqueIndex = __esm({
568
+ "src/utils/uniqueIndex.ts"() {
569
+ "use strict";
570
+ }
571
+ });
572
+
477
573
  // src/models/BaseModel.ts
478
- import * as Y3 from "yjs";
479
- import { ulid as ulid2 } from "ulid";
574
+ import * as Y6 from "yjs";
575
+ import { ulid as ulid3 } from "ulid";
480
576
  function generateULID() {
481
- return ulid2();
577
+ return ulid3();
482
578
  }
483
579
  var init_BaseModel = __esm({
484
580
  "src/models/BaseModel.ts"() {
@@ -489,6 +585,9 @@ var init_BaseModel = __esm({
489
585
  init_CursorManager();
490
586
  init_sql();
491
587
  init_metaSync();
588
+ init_writeLifecycle();
589
+ init_yjsValues();
590
+ init_uniqueIndex();
492
591
  registerFunctionDefault(generateULID, "generate_ulid");
493
592
  }
494
593
  });
@@ -1518,6 +1617,45 @@ var DurableObjectEngine = class extends JsonSchemaEngine {
1518
1617
  init_queryTypes();
1519
1618
  init_CursorManager();
1520
1619
  init_sql();
1620
+
1621
+ // src/query/recordRow.ts
1622
+ var PROJECTED_JSON_ALIAS_PREFIX = "_json_";
1623
+ function projectedJsonAlias(fieldName) {
1624
+ return `${PROJECTED_JSON_ALIAS_PREFIX}${fieldName}`;
1625
+ }
1626
+ function parseRecordRow(row) {
1627
+ const { _id, _type, _data, ...rest } = row;
1628
+ let parsed = {};
1629
+ if (_data) {
1630
+ try {
1631
+ parsed = JSON.parse(_data);
1632
+ } catch (e) {
1633
+ console.warn("Failed to parse _data:", e);
1634
+ }
1635
+ }
1636
+ return { id: _id, type: _type, ...parsed, ...decodeProjectedColumns(rest) };
1637
+ }
1638
+ function decodeProjectedColumns(columns) {
1639
+ const decoded = {};
1640
+ for (const [column, value] of Object.entries(columns)) {
1641
+ if (!column.startsWith(PROJECTED_JSON_ALIAS_PREFIX)) {
1642
+ decoded[column] = value;
1643
+ continue;
1644
+ }
1645
+ decoded[column.slice(PROJECTED_JSON_ALIAS_PREFIX.length)] = parseJsonColumn(value);
1646
+ }
1647
+ return decoded;
1648
+ }
1649
+ function parseJsonColumn(value) {
1650
+ if (typeof value !== "string") return value;
1651
+ try {
1652
+ return JSON.parse(value);
1653
+ } catch {
1654
+ return value;
1655
+ }
1656
+ }
1657
+
1658
+ // src/query/JsonQueryTranslator.ts
1521
1659
  init_patterns();
1522
1660
  var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1523
1661
  var JsonQueryTranslator = class {
@@ -1562,7 +1700,8 @@ var JsonQueryTranslator = class {
1562
1700
  const whereClause = this.translateFilter(filter);
1563
1701
  const orderClause = CursorManager.buildOrderClause(
1564
1702
  options?.sort,
1565
- (field) => this.getFieldSql(field)
1703
+ (field) => this.getFieldSql(field),
1704
+ options?.direction || 1
1566
1705
  );
1567
1706
  const limitClause = this.buildLimitClause(options);
1568
1707
  const paginationClause = this.buildPaginationClause(
@@ -2013,7 +2152,7 @@ var JsonQueryTranslator = class {
2013
2152
  for (const field of includeFields) {
2014
2153
  if (field === "id" || field === "type") continue;
2015
2154
  selectParts.push(
2016
- `json_extract(_data, '$.${field}') AS ${quoteIdentifier(field)}`
2155
+ `_data -> '$.${field}' AS ${quoteIdentifier(projectedJsonAlias(field))}`
2017
2156
  );
2018
2157
  }
2019
2158
  return selectParts.join(", ");
@@ -2200,6 +2339,86 @@ var JsonQueryTranslator = class {
2200
2339
  init_CursorManager();
2201
2340
  init_sql();
2202
2341
  import { ulid } from "ulid";
2342
+ var AtomicBatchAbort = class extends Error {
2343
+ constructor(failedIndex) {
2344
+ super(`atomic batch aborted at operation ${failedIndex}`);
2345
+ this.failedIndex = failedIndex;
2346
+ this.name = "AtomicBatchAbort";
2347
+ }
2348
+ };
2349
+ function checkReservedFieldName(fieldName) {
2350
+ if (fieldName === "id") return null;
2351
+ if (fieldName === "type") {
2352
+ return `Field 'type' is reserved (maps to internal _type column in queries)`;
2353
+ }
2354
+ if (fieldName.startsWith("_")) {
2355
+ return `Field '${fieldName}' is reserved (fields starting with '_' are internal)`;
2356
+ }
2357
+ return null;
2358
+ }
2359
+ function checkAutoStampsReservedFields(autoStamps) {
2360
+ if (!autoStamps) return null;
2361
+ for (const field of autoStamps.create || []) {
2362
+ const err = checkReservedFieldName(field);
2363
+ if (err) return `autoStamps.create: ${err}`;
2364
+ }
2365
+ for (const field of autoStamps.update || []) {
2366
+ const err = checkReservedFieldName(field);
2367
+ if (err) return `autoStamps.update: ${err}`;
2368
+ }
2369
+ return null;
2370
+ }
2371
+ function applyAutoStamps(data, autoStamps, isNew) {
2372
+ if (!autoStamps || !data) return;
2373
+ const reservedError = checkAutoStampsReservedFields(autoStamps);
2374
+ if (reservedError) {
2375
+ throw new Error(reservedError);
2376
+ }
2377
+ const now = Date.now();
2378
+ if (isNew && autoStamps.create) {
2379
+ for (const field of autoStamps.create) {
2380
+ if (data[field] === void 0 || data[field] === null) {
2381
+ data[field] = now;
2382
+ }
2383
+ }
2384
+ }
2385
+ if (autoStamps.update) {
2386
+ for (const field of autoStamps.update) {
2387
+ if (data[field] === void 0 || data[field] === null) {
2388
+ data[field] = now;
2389
+ }
2390
+ }
2391
+ }
2392
+ }
2393
+ function collectAppliedStamps(data, autoStamps, isNew) {
2394
+ if (!autoStamps || !data) return void 0;
2395
+ const stamps = {};
2396
+ const take = (fields) => {
2397
+ if (!fields) return;
2398
+ for (const field of fields) {
2399
+ const value = data[field];
2400
+ if (typeof value === "number" && Number.isFinite(value)) {
2401
+ stamps[field] = value;
2402
+ }
2403
+ }
2404
+ };
2405
+ if (isNew) take(autoStamps.create);
2406
+ take(autoStamps.update);
2407
+ return Object.keys(stamps).length > 0 ? stamps : void 0;
2408
+ }
2409
+ function collectAppliedValues(data, fieldNames) {
2410
+ if (!data || !fieldNames || fieldNames.length === 0) return void 0;
2411
+ const out = {};
2412
+ for (const name of fieldNames) {
2413
+ const value = data[name];
2414
+ if (value !== void 0) out[name] = value;
2415
+ }
2416
+ return Object.keys(out).length > 0 ? out : void 0;
2417
+ }
2418
+ function mergeAppliedFields(stamps, applied) {
2419
+ if (!stamps && !applied) return void 0;
2420
+ return { ...stamps ?? {}, ...applied ?? {} };
2421
+ }
2203
2422
  function createDatabaseDO(config = {}) {
2204
2423
  const hooks = config.hooks;
2205
2424
  return class DocumentDO {
@@ -2357,6 +2576,16 @@ function createDatabaseDO(config = {}) {
2357
2576
  hasMore,
2358
2577
  isFirstPage
2359
2578
  );
2579
+ if (options?.projection && Object.keys(options.projection).length > 0) {
2580
+ const isExclusion = Object.values(options.projection).some(
2581
+ (v) => v === 0
2582
+ );
2583
+ if (isExclusion) {
2584
+ data = data.map(
2585
+ (rec) => this._applyProjection(rec, options.projection)
2586
+ );
2587
+ }
2588
+ }
2360
2589
  const response = {
2361
2590
  data,
2362
2591
  hasMore,
@@ -2380,7 +2609,15 @@ function createDatabaseDO(config = {}) {
2380
2609
  /** @internal */
2381
2610
  async _handleSave(request, docId) {
2382
2611
  const body = await request.json();
2383
- const { modelName, data, stringSets, ifNotExists, condition, upsertOn } = body;
2612
+ const {
2613
+ modelName,
2614
+ data,
2615
+ stringSets,
2616
+ ifNotExists,
2617
+ condition,
2618
+ upsertOn,
2619
+ autoStamps
2620
+ } = body;
2384
2621
  let id = body.id;
2385
2622
  if (!modelName || !id && !upsertOn) {
2386
2623
  return this._errorResponse("modelName is required, and either id or upsertOn must be provided", 400);
@@ -2394,6 +2631,12 @@ function createDatabaseDO(config = {}) {
2394
2631
  return this._errorResponse(reservedError, 400);
2395
2632
  }
2396
2633
  }
2634
+ {
2635
+ const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
2636
+ if (autoStampsReservedError) {
2637
+ return this._errorResponse(autoStampsReservedError, 400);
2638
+ }
2639
+ }
2397
2640
  if (upsertOn) {
2398
2641
  if (data[upsertOn] === void 0 || data[upsertOn] === null || data[upsertOn] === "") {
2399
2642
  return this._errorResponse(
@@ -2443,8 +2686,10 @@ function createDatabaseDO(config = {}) {
2443
2686
  409
2444
2687
  );
2445
2688
  }
2689
+ const isNew = !this._engine.recordExists(modelName, id);
2690
+ applyAutoStamps(data, autoStamps, isNew);
2691
+ let hookAppliedNames;
2446
2692
  if (hooks?.beforeSave) {
2447
- const isNew = !this._engine.recordExists(modelName, id);
2448
2693
  const ctx = {
2449
2694
  modelName,
2450
2695
  docId,
@@ -2459,6 +2704,7 @@ function createDatabaseDO(config = {}) {
2459
2704
  if (!result.allow) {
2460
2705
  return this._errorResponse(result.reason || "Save denied", 403);
2461
2706
  }
2707
+ hookAppliedNames = result.serverAppliedFieldNames;
2462
2708
  }
2463
2709
  const existingForUnique = this._engine.execSqlSync(
2464
2710
  "SELECT _data FROM records WHERE _id = ? AND _type = ?",
@@ -2488,12 +2734,17 @@ function createDatabaseDO(config = {}) {
2488
2734
  }
2489
2735
  this._engine.trackModelFields(modelName, data);
2490
2736
  const response = { success: true, id };
2737
+ const appliedFields = mergeAppliedFields(
2738
+ collectAppliedStamps(data, autoStamps, isNew),
2739
+ collectAppliedValues(data, hookAppliedNames)
2740
+ );
2741
+ if (appliedFields) response.appliedFields = appliedFields;
2491
2742
  return Response.json(response);
2492
2743
  }
2493
2744
  /** @internal — Partial update: merge provided fields into existing record */
2494
2745
  async _handlePatch(request, docId) {
2495
2746
  const body = await request.json();
2496
- const { modelName, id, data, stringSets, condition } = body;
2747
+ const { modelName, id, data, stringSets, condition, autoStamps } = body;
2497
2748
  if (!modelName || !id || !data) {
2498
2749
  return this._errorResponse("modelName, id, and data are required", 400);
2499
2750
  }
@@ -2501,6 +2752,12 @@ function createDatabaseDO(config = {}) {
2501
2752
  if (reservedError) {
2502
2753
  return this._errorResponse(reservedError, 400);
2503
2754
  }
2755
+ {
2756
+ const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
2757
+ if (autoStampsReservedError) {
2758
+ return this._errorResponse(autoStampsReservedError, 400);
2759
+ }
2760
+ }
2504
2761
  if (!this._engine.recordExists(modelName, id)) {
2505
2762
  return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2506
2763
  }
@@ -2510,6 +2767,8 @@ function createDatabaseDO(config = {}) {
2510
2767
  409
2511
2768
  );
2512
2769
  }
2770
+ applyAutoStamps(data, autoStamps, false);
2771
+ let hookAppliedNames;
2513
2772
  if (hooks?.beforeSave) {
2514
2773
  const ctx = {
2515
2774
  modelName,
@@ -2525,6 +2784,7 @@ function createDatabaseDO(config = {}) {
2525
2784
  if (!result.allow) {
2526
2785
  return this._errorResponse(result.reason || "Save denied", 403);
2527
2786
  }
2787
+ hookAppliedNames = result.serverAppliedFieldNames;
2528
2788
  }
2529
2789
  const existing = this._engine.execSqlSync(
2530
2790
  "SELECT _data FROM records WHERE _id = ? AND _type = ?",
@@ -2548,6 +2808,11 @@ function createDatabaseDO(config = {}) {
2548
2808
  }
2549
2809
  this._engine.trackModelFields(modelName, data);
2550
2810
  const response = { success: true, id };
2811
+ const appliedFields = mergeAppliedFields(
2812
+ collectAppliedStamps(data, autoStamps, false),
2813
+ collectAppliedValues(data, hookAppliedNames)
2814
+ );
2815
+ if (appliedFields) response.appliedFields = appliedFields;
2551
2816
  return Response.json(response);
2552
2817
  }
2553
2818
  /** @internal */
@@ -2589,13 +2854,16 @@ function createDatabaseDO(config = {}) {
2589
2854
  /** @internal — Execute multiple save/patch/delete operations in a single transaction */
2590
2855
  async _handleBatch(request, docId) {
2591
2856
  const body = await request.json();
2592
- const { operations } = body;
2857
+ const { operations, atomic = false } = body;
2593
2858
  if (!operations || !Array.isArray(operations) || operations.length === 0) {
2594
2859
  return this._errorResponse("operations array is required and must not be empty", 400);
2595
2860
  }
2596
2861
  const hookDenials = /* @__PURE__ */ new Map();
2597
2862
  const resolvedBatchIds = /* @__PURE__ */ new Map();
2598
2863
  const opErrors = /* @__PURE__ */ new Map();
2864
+ const batchCreatedIds = /* @__PURE__ */ new Set();
2865
+ const batchIsNew = /* @__PURE__ */ new Map();
2866
+ const batchAppliedNames = /* @__PURE__ */ new Map();
2599
2867
  for (let i = 0; i < operations.length; i++) {
2600
2868
  const op = operations[i];
2601
2869
  if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
@@ -2647,9 +2915,31 @@ function createDatabaseDO(config = {}) {
2647
2915
  400
2648
2916
  );
2649
2917
  }
2918
+ const autoStampsReservedError = checkAutoStampsReservedFields(
2919
+ op.autoStamps
2920
+ );
2921
+ if (autoStampsReservedError) {
2922
+ return this._errorResponse(
2923
+ `Operation ${i}: ${autoStampsReservedError}`,
2924
+ 400
2925
+ );
2926
+ }
2927
+ const effectiveId = resolvedBatchIds.get(i) || op.id || "";
2928
+ const isNew = op.op === "save" ? (
2929
+ // Codex P2 #3: also treat the id as not-new if a previous
2930
+ // op in this batch already resolved to a save for the same
2931
+ // model+id. Without this, two saves to the same fresh id
2932
+ // both compute isNew=true against the pre-transaction
2933
+ // database, so a `'create'` autoStamp on the second op
2934
+ // would overwrite the first op's stamp.
2935
+ !batchCreatedIds.has(`${op.modelName}/${effectiveId}`) && !this._engine.recordExists(op.modelName, effectiveId)
2936
+ ) : false;
2937
+ if (op.op === "save" && isNew && effectiveId) {
2938
+ batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
2939
+ }
2940
+ batchIsNew.set(i, isNew);
2941
+ applyAutoStamps(op.data, op.autoStamps, isNew);
2650
2942
  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
2943
  const ctx = {
2654
2944
  modelName: op.modelName,
2655
2945
  docId,
@@ -2663,6 +2953,8 @@ function createDatabaseDO(config = {}) {
2663
2953
  const result = await hooks.beforeSave(ctx);
2664
2954
  if (!result.allow) {
2665
2955
  hookDenials.set(i, result.reason || "Save denied");
2956
+ } else if (result.serverAppliedFieldNames) {
2957
+ batchAppliedNames.set(i, result.serverAppliedFieldNames);
2666
2958
  }
2667
2959
  }
2668
2960
  } else if (op.op === "delete") {
@@ -2711,205 +3003,263 @@ function createDatabaseDO(config = {}) {
2711
3003
  }
2712
3004
  const results = [];
2713
3005
  const fieldsToTrack = [];
2714
- this._engine.transactionSync(() => {
2715
- for (let i = 0; i < operations.length; i++) {
2716
- const op = operations[i];
2717
- if (hookDenials.has(i)) {
2718
- const effectiveId = resolvedBatchIds.get(i) || op.id || "";
2719
- results.push({
2720
- success: false,
2721
- id: effectiveId,
2722
- error: hookDenials.get(i)
2723
- });
2724
- continue;
2725
- }
2726
- if (opErrors.has(i)) {
2727
- results.push({
2728
- success: false,
2729
- id: op.id || "",
2730
- error: opErrors.get(i)
2731
- });
2732
- continue;
2733
- }
2734
- const conditionId = resolvedBatchIds.get(i) || op.id;
2735
- if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
2736
- results.push({
2737
- success: false,
2738
- id: conditionId,
2739
- error: `Condition not met for ${op.modelName}/${conditionId}`
2740
- });
2741
- continue;
2742
- }
2743
- if (op.op === "save") {
2744
- let opId = resolvedBatchIds.get(i) || op.id;
2745
- if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
2746
- results.push({
2747
- success: false,
2748
- id: opId,
2749
- error: `Record ${op.modelName}/${opId} already exists`
3006
+ const pushFailure = (index, failure) => {
3007
+ results.push({
3008
+ success: false,
3009
+ id: failure.id,
3010
+ error: failure.error,
3011
+ status: failure.status,
3012
+ code: failure.code
3013
+ });
3014
+ if (atomic) throw new AtomicBatchAbort(index);
3015
+ };
3016
+ try {
3017
+ this._engine.transactionSync(() => {
3018
+ for (let i = 0; i < operations.length; i++) {
3019
+ const op = operations[i];
3020
+ if (hookDenials.has(i)) {
3021
+ const effectiveId = resolvedBatchIds.get(i) || op.id || "";
3022
+ pushFailure(i, {
3023
+ id: effectiveId,
3024
+ error: hookDenials.get(i),
3025
+ status: 403,
3026
+ code: "HOOK_DENIED"
2750
3027
  });
2751
3028
  continue;
2752
3029
  }
2753
- if (op.data) {
2754
- const existingForUnique = this._engine.execSqlSync(
2755
- "SELECT _data FROM records WHERE _id = ? AND _type = ?",
2756
- [opId, op.modelName]
2757
- );
2758
- let dataForUnique = op.data;
2759
- if (existingForUnique.length > 0) {
2760
- const existingData = JSON.parse(existingForUnique[0]._data);
2761
- dataForUnique = { ...existingData, ...op.data };
2762
- }
2763
- const violation = this._engine.checkUniqueConstraints(
2764
- op.modelName,
2765
- opId,
2766
- dataForUnique
2767
- );
2768
- if (violation) {
2769
- results.push({
2770
- success: false,
3030
+ if (opErrors.has(i)) {
3031
+ pushFailure(i, {
3032
+ id: op.id || "",
3033
+ error: opErrors.get(i),
3034
+ status: 400,
3035
+ code: "VALIDATION_ERROR"
3036
+ });
3037
+ continue;
3038
+ }
3039
+ const conditionId = resolvedBatchIds.get(i) || op.id;
3040
+ if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
3041
+ pushFailure(i, {
3042
+ id: conditionId,
3043
+ error: `Condition not met for ${op.modelName}/${conditionId}`,
3044
+ status: 409,
3045
+ code: "CONDITION_NOT_MET"
3046
+ });
3047
+ continue;
3048
+ }
3049
+ if (op.op === "save") {
3050
+ let opId = resolvedBatchIds.get(i) || op.id;
3051
+ if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
3052
+ pushFailure(i, {
2771
3053
  id: opId,
2772
- error: violation
3054
+ error: `Record ${op.modelName}/${opId} already exists`,
3055
+ status: 409,
3056
+ code: "ALREADY_EXISTS"
2773
3057
  });
2774
3058
  continue;
2775
3059
  }
2776
- }
2777
- if (op.stringSets && Object.keys(op.stringSets).length > 0) {
2778
- const { id: _id, ...fieldsForJson } = op.data;
2779
- const jsonFields = { ...fieldsForJson, ...op.stringSets };
2780
- const dataJson = JSON.stringify(jsonFields);
2781
- this._engine.execSqlSync(
2782
- "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
2783
- [opId, op.modelName, dataJson]
2784
- );
2785
- for (const [fieldName, values] of Object.entries(op.stringSets)) {
2786
- this._engine.execSqlSync(
2787
- "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
2788
- [op.modelName, opId, fieldName]
3060
+ if (op.data) {
3061
+ const existingForUnique = this._engine.execSqlSync(
3062
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
3063
+ [opId, op.modelName]
2789
3064
  );
2790
- for (const value of values) {
2791
- this._engine.execSqlSync(
2792
- "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
2793
- [opId, op.modelName, fieldName, value]
2794
- );
3065
+ let dataForUnique = op.data;
3066
+ if (existingForUnique.length > 0) {
3067
+ const existingData = JSON.parse(existingForUnique[0]._data);
3068
+ dataForUnique = { ...existingData, ...op.data };
2795
3069
  }
2796
- }
2797
- } else {
2798
- const { id: _id, ...fieldsForJson } = op.data;
2799
- const dataJson = JSON.stringify(fieldsForJson);
2800
- this._engine.execSqlSync(
2801
- "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
2802
- [opId, op.modelName, dataJson]
2803
- );
2804
- }
2805
- if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
2806
- results.push({ success: true, id: opId });
2807
- } else if (op.op === "patch") {
2808
- if (!this._engine.recordExists(op.modelName, op.id)) {
2809
- results.push({
2810
- success: false,
2811
- id: op.id,
2812
- error: `Record ${op.modelName}/${op.id} not found`
2813
- });
2814
- continue;
2815
- }
2816
- if (op.data) {
2817
- const existing = this._engine.execSqlSync(
2818
- "SELECT _data FROM records WHERE _id = ? AND _type = ?",
2819
- [op.id, op.modelName]
2820
- );
2821
- if (existing.length > 0) {
2822
- const existingData = JSON.parse(existing[0]._data);
2823
- const mergedData = { ...existingData, ...op.data };
2824
3070
  const violation = this._engine.checkUniqueConstraints(
2825
3071
  op.modelName,
2826
- op.id,
2827
- mergedData
3072
+ opId,
3073
+ dataForUnique
2828
3074
  );
2829
3075
  if (violation) {
2830
- results.push({
2831
- success: false,
2832
- id: op.id,
2833
- error: violation
3076
+ pushFailure(i, {
3077
+ id: opId,
3078
+ error: violation,
3079
+ status: 409,
3080
+ code: "UNIQUE_VIOLATION"
2834
3081
  });
2835
3082
  continue;
2836
3083
  }
2837
3084
  }
2838
- }
2839
- const found = this._engine.patchRecordRaw(
2840
- op.modelName,
2841
- op.id,
2842
- op.data,
2843
- op.stringSets
2844
- );
2845
- if (!found) {
2846
- results.push({
2847
- success: false,
2848
- id: op.id,
2849
- error: `Record ${op.modelName}/${op.id} not found`
2850
- });
2851
- } else {
3085
+ if (op.stringSets && Object.keys(op.stringSets).length > 0) {
3086
+ const { id: _id, ...fieldsForJson } = op.data;
3087
+ const jsonFields = { ...fieldsForJson, ...op.stringSets };
3088
+ const dataJson = JSON.stringify(jsonFields);
3089
+ this._engine.execSqlSync(
3090
+ "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
3091
+ [opId, op.modelName, dataJson]
3092
+ );
3093
+ for (const [fieldName, values] of Object.entries(op.stringSets)) {
3094
+ this._engine.execSqlSync(
3095
+ "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
3096
+ [op.modelName, opId, fieldName]
3097
+ );
3098
+ for (const value of values) {
3099
+ this._engine.execSqlSync(
3100
+ "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
3101
+ [opId, op.modelName, fieldName, value]
3102
+ );
3103
+ }
3104
+ }
3105
+ } else {
3106
+ const { id: _id, ...fieldsForJson } = op.data;
3107
+ const dataJson = JSON.stringify(fieldsForJson);
3108
+ this._engine.execSqlSync(
3109
+ "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
3110
+ [opId, op.modelName, dataJson]
3111
+ );
3112
+ }
2852
3113
  if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
3114
+ const saveResult = { success: true, id: opId };
3115
+ const saveApplied = mergeAppliedFields(
3116
+ collectAppliedStamps(
3117
+ op.data,
3118
+ op.autoStamps,
3119
+ batchIsNew.get(i) ?? false
3120
+ ),
3121
+ collectAppliedValues(op.data, batchAppliedNames.get(i))
3122
+ );
3123
+ if (saveApplied) saveResult.appliedFields = saveApplied;
3124
+ results.push(saveResult);
3125
+ } else if (op.op === "patch") {
3126
+ if (!this._engine.recordExists(op.modelName, op.id)) {
3127
+ pushFailure(i, {
3128
+ id: op.id,
3129
+ error: `Record ${op.modelName}/${op.id} not found`,
3130
+ status: 404,
3131
+ code: "NOT_FOUND"
3132
+ });
3133
+ continue;
3134
+ }
3135
+ if (op.data) {
3136
+ const existing = this._engine.execSqlSync(
3137
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
3138
+ [op.id, op.modelName]
3139
+ );
3140
+ if (existing.length > 0) {
3141
+ const existingData = JSON.parse(existing[0]._data);
3142
+ const mergedData = { ...existingData, ...op.data };
3143
+ const violation = this._engine.checkUniqueConstraints(
3144
+ op.modelName,
3145
+ op.id,
3146
+ mergedData
3147
+ );
3148
+ if (violation) {
3149
+ pushFailure(i, {
3150
+ id: op.id,
3151
+ error: violation,
3152
+ status: 409,
3153
+ code: "UNIQUE_VIOLATION"
3154
+ });
3155
+ continue;
3156
+ }
3157
+ }
3158
+ }
3159
+ const found = this._engine.patchRecordRaw(
3160
+ op.modelName,
3161
+ op.id,
3162
+ op.data,
3163
+ op.stringSets
3164
+ );
3165
+ if (!found) {
3166
+ pushFailure(i, {
3167
+ id: op.id,
3168
+ error: `Record ${op.modelName}/${op.id} not found`,
3169
+ status: 404,
3170
+ code: "NOT_FOUND"
3171
+ });
3172
+ } else {
3173
+ if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
3174
+ const patchResult = {
3175
+ success: true,
3176
+ id: op.id
3177
+ };
3178
+ const patchApplied = mergeAppliedFields(
3179
+ collectAppliedStamps(op.data, op.autoStamps, false),
3180
+ collectAppliedValues(op.data, batchAppliedNames.get(i))
3181
+ );
3182
+ if (patchApplied) patchResult.appliedFields = patchApplied;
3183
+ results.push(patchResult);
3184
+ }
3185
+ } else if (op.op === "delete") {
3186
+ this._engine.execSqlSync(
3187
+ "DELETE FROM records WHERE _id = ? AND _type = ?",
3188
+ [op.id, op.modelName]
3189
+ );
3190
+ this._engine.execSqlSync(
3191
+ "DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
3192
+ [op.id, op.modelName]
3193
+ );
3194
+ results.push({ success: true, id: op.id });
3195
+ } else if (op.op === "increment") {
3196
+ const newValues = this._engine.incrementFieldsRaw(
3197
+ op.modelName,
3198
+ op.id,
3199
+ op.fields
3200
+ );
3201
+ if (!newValues) {
3202
+ pushFailure(i, {
3203
+ id: op.id,
3204
+ error: `Record ${op.modelName}/${op.id} not found`,
3205
+ status: 404,
3206
+ code: "NOT_FOUND"
3207
+ });
3208
+ } else {
3209
+ results.push({ success: true, id: op.id, values: newValues });
3210
+ }
3211
+ } else if (op.op === "addToSet") {
3212
+ if (!this._engine.recordExists(op.modelName, op.id)) {
3213
+ pushFailure(i, {
3214
+ id: op.id,
3215
+ error: `Record ${op.modelName}/${op.id} not found`,
3216
+ status: 404,
3217
+ code: "NOT_FOUND"
3218
+ });
3219
+ continue;
3220
+ }
3221
+ this._engine.addToStringSetsRaw(
3222
+ op.modelName,
3223
+ op.id,
3224
+ op.stringSets
3225
+ );
3226
+ results.push({ success: true, id: op.id });
3227
+ } else if (op.op === "removeFromSet") {
3228
+ if (!this._engine.recordExists(op.modelName, op.id)) {
3229
+ pushFailure(i, {
3230
+ id: op.id,
3231
+ error: `Record ${op.modelName}/${op.id} not found`,
3232
+ status: 404,
3233
+ code: "NOT_FOUND"
3234
+ });
3235
+ continue;
3236
+ }
3237
+ this._engine.removeFromStringSetsRaw(
3238
+ op.modelName,
3239
+ op.id,
3240
+ op.stringSets
3241
+ );
2853
3242
  results.push({ success: true, id: op.id });
2854
3243
  }
2855
- } else if (op.op === "delete") {
2856
- this._engine.execSqlSync(
2857
- "DELETE FROM records WHERE _id = ? AND _type = ?",
2858
- [op.id, op.modelName]
2859
- );
2860
- this._engine.execSqlSync(
2861
- "DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
2862
- [op.id, op.modelName]
2863
- );
2864
- results.push({ success: true, id: op.id });
2865
- } else if (op.op === "increment") {
2866
- const newValues = this._engine.incrementFieldsRaw(
2867
- op.modelName,
2868
- op.id,
2869
- op.fields
2870
- );
2871
- if (!newValues) {
2872
- results.push({
2873
- success: false,
2874
- id: op.id,
2875
- error: `Record ${op.modelName}/${op.id} not found`
2876
- });
2877
- } else {
2878
- results.push({ success: true, id: op.id, values: newValues });
2879
- }
2880
- } else if (op.op === "addToSet") {
2881
- if (!this._engine.recordExists(op.modelName, op.id)) {
2882
- results.push({
2883
- success: false,
2884
- id: op.id,
2885
- error: `Record ${op.modelName}/${op.id} not found`
2886
- });
2887
- continue;
2888
- }
2889
- this._engine.addToStringSetsRaw(
2890
- op.modelName,
2891
- op.id,
2892
- op.stringSets
2893
- );
2894
- results.push({ success: true, id: op.id });
2895
- } else if (op.op === "removeFromSet") {
2896
- if (!this._engine.recordExists(op.modelName, op.id)) {
2897
- results.push({
2898
- success: false,
2899
- id: op.id,
2900
- error: `Record ${op.modelName}/${op.id} not found`
2901
- });
2902
- continue;
2903
- }
2904
- this._engine.removeFromStringSetsRaw(
2905
- op.modelName,
2906
- op.id,
2907
- op.stringSets
2908
- );
2909
- results.push({ success: true, id: op.id });
2910
3244
  }
3245
+ });
3246
+ } catch (err) {
3247
+ if (err instanceof AtomicBatchAbort) {
3248
+ const failed = results[err.failedIndex];
3249
+ const failedOp = operations[err.failedIndex];
3250
+ console.warn(
3251
+ `[js-bao batch] atomic rollback: operation ${err.failedIndex} (${failedOp?.op}) failed \u2014 ${failed?.error ?? "unknown error"}`
3252
+ );
3253
+ const errorBody = {
3254
+ error: `Operation ${err.failedIndex} (${failedOp?.op}) failed: ${failed?.error ?? "unknown error"}`,
3255
+ failedIndex: err.failedIndex,
3256
+ op: failedOp?.op,
3257
+ code: failed?.code
3258
+ };
3259
+ return Response.json(errorBody, { status: failed?.status ?? 409 });
2911
3260
  }
2912
- });
3261
+ throw err;
3262
+ }
2913
3263
  for (const entry of fieldsToTrack) {
2914
3264
  this._engine.trackModelFields(entry.modelName, entry.data);
2915
3265
  }
@@ -3279,6 +3629,18 @@ function createDatabaseDO(config = {}) {
3279
3629
  }
3280
3630
  return facetResult;
3281
3631
  }
3632
+ if (aggOptions.groupBy.length === 0) {
3633
+ const row = results[0];
3634
+ const flat = {};
3635
+ for (const op of aggOptions.operations) {
3636
+ if (op.type === "count") {
3637
+ flat.count = row.count;
3638
+ } else {
3639
+ flat[`${op.type}_${op.field}`] = row[`${op.type}_${op.field}`];
3640
+ }
3641
+ }
3642
+ return flat;
3643
+ }
3282
3644
  const nestedResult = {};
3283
3645
  const aliasLookup = /* @__PURE__ */ new Map();
3284
3646
  for (const entry of aliasMap) {
@@ -3606,16 +3968,7 @@ function createDatabaseDO(config = {}) {
3606
3968
  }
3607
3969
  /** @internal */
3608
3970
  _parseRow(row) {
3609
- const { _id, _type, _data, ...rest } = row;
3610
- let parsed = {};
3611
- if (_data) {
3612
- try {
3613
- parsed = JSON.parse(_data);
3614
- } catch (e) {
3615
- console.warn("Failed to parse _data:", e);
3616
- }
3617
- }
3618
- return { id: _id, type: _type, ...parsed, ...rest };
3971
+ return parseRecordRow(row);
3619
3972
  }
3620
3973
  // ─── Include (Related Data Loading) ──────────────────────────────
3621
3974
  /** @internal — Validate an IncludeSpec, returning an error message or null */
@@ -3745,8 +4098,9 @@ function createDatabaseDO(config = {}) {
3745
4098
  spec.filter,
3746
4099
  { projection: spec.projection }
3747
4100
  );
4101
+ const relatedProjected = spec.projection ? related.map((r) => this._applyProjection(r, spec.projection)) : related;
3748
4102
  const lookupMap = /* @__PURE__ */ new Map();
3749
- for (const r of related) {
4103
+ for (const r of relatedProjected) {
3750
4104
  lookupMap.set(r.id, r);
3751
4105
  }
3752
4106
  for (const rec of records) {
@@ -3779,8 +4133,9 @@ function createDatabaseDO(config = {}) {
3779
4133
  spec.filter,
3780
4134
  { projection: spec.projection }
3781
4135
  );
4136
+ const targetsProjected = spec.projection ? targets.map((t) => this._applyProjection(t, spec.projection)) : targets;
3782
4137
  const targetLookup = /* @__PURE__ */ new Map();
3783
- for (const t of targets) {
4138
+ for (const t of targetsProjected) {
3784
4139
  targetLookup.set(t.id, t);
3785
4140
  }
3786
4141
  for (const rec of records) {
@@ -3924,6 +4279,14 @@ function createDatabaseDO(config = {}) {
3924
4279
  return allResults;
3925
4280
  }
3926
4281
  /** @internal — Apply projection to a single record */
4282
+ // Apply a projection to a single record. Assumes the projection has
4283
+ // already been validated upstream (JsonQueryTranslator.validateProjection /
4284
+ // buildSelectClause reject mixed inclusion+exclusion): `isInclusion` treats
4285
+ // "any value === 1" as inclusion mode, so an unvalidated mixed projection
4286
+ // would silently ignore the excludes. `id` and `type` are system-identity
4287
+ // fields and are always kept in BOTH modes (a record without its `id` is
4288
+ // unusable downstream). `_meta` is NOT forced — an explicit `{ _meta: 0 }`
4289
+ // is honored.
3927
4290
  _applyProjection(record, projection) {
3928
4291
  const fields = Object.entries(projection);
3929
4292
  if (fields.length === 0) return record;
@@ -3942,7 +4305,7 @@ function createDatabaseDO(config = {}) {
3942
4305
  } else {
3943
4306
  const result = { ...record };
3944
4307
  for (const [f, v] of fields) {
3945
- if (v === 0) delete result[f];
4308
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
3946
4309
  }
3947
4310
  return result;
3948
4311
  }
@@ -4010,7 +4373,8 @@ async function handleRequest(request, env) {
4010
4373
  }
4011
4374
 
4012
4375
  // src/utils/yDocSchema.ts
4013
- import * as Y from "yjs";
4376
+ init_yjsValues();
4377
+ import * as Y2 from "yjs";
4014
4378
  function discoverSchema(yDoc) {
4015
4379
  const models = {};
4016
4380
  const metaNames = /* @__PURE__ */ new Set();
@@ -4044,7 +4408,7 @@ function discoverModelNames(yDoc) {
4044
4408
  function materializeMap(yDoc, key) {
4045
4409
  try {
4046
4410
  const map = yDoc.getMap(key);
4047
- return map instanceof Y.Map ? map : null;
4411
+ return map instanceof Y2.Map ? map : null;
4048
4412
  } catch {
4049
4413
  return null;
4050
4414
  }
@@ -4054,11 +4418,11 @@ function readModelMeta(metaMap) {
4054
4418
  let constraints;
4055
4419
  let relationships;
4056
4420
  for (const [key, value] of metaMap.entries()) {
4057
- if (key === "_constraints" && value instanceof Y.Map) {
4421
+ if (key === "_constraints" && value instanceof Y2.Map) {
4058
4422
  constraints = readConstraints(value);
4059
- } else if (key === "_relationships" && value instanceof Y.Map) {
4423
+ } else if (key === "_relationships" && value instanceof Y2.Map) {
4060
4424
  relationships = readRelationships(value);
4061
- } else if (value instanceof Y.Map) {
4425
+ } else if (value instanceof Y2.Map) {
4062
4426
  fields[key] = readFieldMeta(value);
4063
4427
  }
4064
4428
  }
@@ -4077,6 +4441,10 @@ function readFieldMeta(fieldMap) {
4077
4441
  if (fieldMap.get("unique") === true) field.unique = true;
4078
4442
  if (fieldMap.get("required") === true) field.required = true;
4079
4443
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
4444
+ const autoStamp = fieldMap.get("autoStamp");
4445
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
4446
+ field.autoStamp = autoStamp;
4447
+ }
4080
4448
  const def = fieldMap.get("default");
4081
4449
  if (def !== void 0) field.default = def;
4082
4450
  const maxLength = fieldMap.get("maxLength");
@@ -4089,7 +4457,7 @@ function inferModelFromData(dataMap) {
4089
4457
  const fields = {};
4090
4458
  let sampled = 0;
4091
4459
  for (const [_recordId, recordValue] of dataMap.entries()) {
4092
- if (!(recordValue instanceof Y.Map)) continue;
4460
+ if (!(recordValue instanceof Y2.Map)) continue;
4093
4461
  if (++sampled > 5) break;
4094
4462
  for (const [fieldName, value] of recordValue.entries()) {
4095
4463
  if (fieldName.startsWith("_")) continue;
@@ -4102,7 +4470,7 @@ function inferModelFromData(dataMap) {
4102
4470
  return { fields };
4103
4471
  }
4104
4472
  function inferTypeFromValue(value) {
4105
- if (value instanceof Y.Map) return "stringset";
4473
+ if (isStringSetYMap(value)) return "stringset";
4106
4474
  switch (typeof value) {
4107
4475
  case "string":
4108
4476
  return "string";
@@ -4117,7 +4485,7 @@ function inferTypeFromValue(value) {
4117
4485
  function readConstraints(constraintsMap) {
4118
4486
  const out = {};
4119
4487
  for (const [name, value] of constraintsMap.entries()) {
4120
- if (!(value instanceof Y.Map)) continue;
4488
+ if (!(value instanceof Y2.Map)) continue;
4121
4489
  let fields = [];
4122
4490
  const rawFields = value.get("fields");
4123
4491
  if (typeof rawFields === "string") {
@@ -4136,7 +4504,7 @@ function readConstraints(constraintsMap) {
4136
4504
  function readRelationships(relsMap) {
4137
4505
  const out = {};
4138
4506
  for (const [name, value] of relsMap.entries()) {
4139
- if (!(value instanceof Y.Map)) continue;
4507
+ if (!(value instanceof Y2.Map)) continue;
4140
4508
  const rel = {};
4141
4509
  for (const [k, v] of value.entries()) {
4142
4510
  rel[k] = v;
@@ -4147,6 +4515,7 @@ function readRelationships(relsMap) {
4147
4515
  }
4148
4516
  var CAMEL_TO_SNAKE = {
4149
4517
  autoAssign: "auto_assign",
4518
+ autoStamp: "auto_stamp",
4150
4519
  maxLength: "max_length",
4151
4520
  maxCount: "max_count",
4152
4521
  relatedIdField: "related_id_field",
@@ -4177,6 +4546,7 @@ function schemaToToml(schema) {
4177
4546
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
4178
4547
  lines.push(`type = ${tomlValue(field.type)}`);
4179
4548
  if (field.autoAssign) lines.push("auto_assign = true");
4549
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
4180
4550
  if (field.indexed) lines.push("indexed = true");
4181
4551
  if (field.unique) lines.push("unique = true");
4182
4552
  if (field.required) lines.push("required = true");
@@ -4213,6 +4583,7 @@ import { parse as parseToml } from "smol-toml";
4213
4583
  // src/models/schema.ts
4214
4584
  init_BaseModel();
4215
4585
  init_ModelRegistry();
4586
+ init_uniqueIndex();
4216
4587
  function defineModelSchema(input) {
4217
4588
  const { name, fields } = input;
4218
4589
  const options = {
@@ -4255,7 +4626,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
4255
4626
  for (const [fieldName, options] of fields.entries()) {
4256
4627
  if (options.unique) {
4257
4628
  resolved.push({
4258
- name: `${modelName}_${fieldName}_unique`,
4629
+ name: fieldUniqueConstraintName(modelName, fieldName),
4259
4630
  fields: [fieldName]
4260
4631
  });
4261
4632
  }
@@ -4300,10 +4671,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
4300
4671
  "unique",
4301
4672
  "required",
4302
4673
  "auto_assign",
4674
+ "auto_stamp",
4303
4675
  "max_length",
4304
4676
  "max_count",
4305
- "default"
4677
+ "default",
4678
+ "enum"
4306
4679
  ]);
4680
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
4307
4681
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
4308
4682
  "fields",
4309
4683
  "relationships",
@@ -4345,9 +4719,37 @@ function parseFieldOptions(raw, context, strict) {
4345
4719
  if (raw.unique === true) opts.unique = true;
4346
4720
  if (raw.required === true) opts.required = true;
4347
4721
  if (raw.auto_assign === true) opts.autoAssign = true;
4722
+ if (raw.auto_stamp !== void 0) {
4723
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
4724
+ throw new Error(
4725
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
4726
+ ...VALID_AUTO_STAMP_VALUES
4727
+ ].join(", ")}`
4728
+ );
4729
+ }
4730
+ opts.autoStamp = raw.auto_stamp;
4731
+ }
4348
4732
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
4349
4733
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
4350
4734
  if (raw.default !== void 0) opts.default = raw.default;
4735
+ if (raw.enum !== void 0) {
4736
+ if (raw.type !== "string") {
4737
+ throw new Error(
4738
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
4739
+ );
4740
+ }
4741
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
4742
+ throw new Error(
4743
+ `${context}: \`enum\` must be a non-empty array of strings.`
4744
+ );
4745
+ }
4746
+ if (!raw.enum.every((v) => typeof v === "string")) {
4747
+ throw new Error(
4748
+ `${context}: \`enum\` values must all be strings.`
4749
+ );
4750
+ }
4751
+ opts.enum = [...raw.enum];
4752
+ }
4351
4753
  return opts;
4352
4754
  }
4353
4755
  function requireField(raw, field, context) {
@@ -4471,18 +4873,26 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
4471
4873
 
4472
4874
  // src/cloudflare-do.ts
4473
4875
  init_metaSync();
4876
+ init_sql();
4474
4877
  export {
4475
4878
  DurableObjectEngine,
4476
4879
  JsonQueryTranslator,
4477
4880
  JsonSchemaDDL,
4881
+ PROJECTED_JSON_ALIAS_PREFIX,
4882
+ buildSafeAlias,
4883
+ checkAutoStampsReservedFields,
4884
+ checkReservedFieldName,
4478
4885
  clearMetaSyncCache,
4479
4886
  createDatabaseDO,
4480
4887
  createDocumentDO,
4888
+ deterministicIdentifierHash,
4481
4889
  discoverModelNames,
4482
4890
  discoverSchema,
4483
4891
  handleRequest,
4484
4892
  inferFieldType,
4485
4893
  loadSchemaFromTomlString,
4894
+ parseRecordRow,
4895
+ projectedJsonAlias,
4486
4896
  registerFunctionDefault,
4487
4897
  schemaToToml,
4488
4898
  syncInferredMeta,