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.
- package/README.md +124 -30
- package/dist/browser.cjs +759 -273
- package/dist/browser.d.cts +62 -0
- package/dist/browser.d.ts +62 -0
- package/dist/browser.js +760 -271
- package/dist/client.cjs +53 -6
- package/dist/client.d.cts +149 -0
- package/dist/client.d.ts +149 -0
- package/dist/client.js +49 -4
- package/dist/cloudflare-do.cjs +643 -231
- package/dist/cloudflare-do.d.cts +237 -1
- package/dist/cloudflare-do.d.ts +237 -1
- package/dist/cloudflare-do.js +639 -229
- package/dist/cloudflare.cjs +330 -26
- package/dist/cloudflare.d.cts +360 -1
- package/dist/cloudflare.d.ts +360 -1
- package/dist/cloudflare.js +326 -24
- package/dist/codegen-v2.cjs +139 -21
- package/dist/codegen.cjs +7 -1
- package/dist/index.cjs +724 -232
- package/dist/index.d.cts +98 -1
- package/dist/index.d.ts +98 -1
- package/dist/index.js +721 -229
- package/dist/node.cjs +948 -299
- package/dist/node.d.cts +220 -1
- package/dist/node.d.ts +220 -1
- package/dist/node.js +934 -296
- package/package.json +2 -1
package/dist/cloudflare-do.cjs
CHANGED
|
@@ -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"() {
|
|
@@ -241,21 +256,36 @@ var init_CursorManager = __esm({
|
|
|
241
256
|
return fields;
|
|
242
257
|
}
|
|
243
258
|
/**
|
|
244
|
-
* Build ORDER BY clause from sort specification
|
|
259
|
+
* Build ORDER BY clause from sort specification.
|
|
260
|
+
*
|
|
261
|
+
* `direction` is the pagination direction (1 = forward, -1 = backward).
|
|
262
|
+
* For backward pagination each field's effective ORDER BY direction is
|
|
263
|
+
* reversed so the cursor's flipped inequality (built by
|
|
264
|
+
* `buildPaginationConditions`) returns the page adjacent to the cursor,
|
|
265
|
+
* not the globally smallest/largest window. Without this,
|
|
266
|
+
* `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
|
|
267
|
+
* rows overall instead of the n rows just below the cursor.
|
|
268
|
+
*
|
|
269
|
+
* The returned `directions` array carries the *declared* per-field sort
|
|
270
|
+
* directions (unaffected by `direction`) — `buildPaginationConditions`
|
|
271
|
+
* needs the declared directions to pick each field's comparison operator.
|
|
245
272
|
*/
|
|
246
|
-
static buildOrderClause(sort, fieldFormatter = (field) => field) {
|
|
273
|
+
static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
|
|
247
274
|
const fields = this.extractSortFields(sort);
|
|
248
275
|
const clauses = [];
|
|
249
276
|
const directions = [];
|
|
277
|
+
const sqlDir = (declared) => {
|
|
278
|
+
const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
|
|
279
|
+
return effective === 1 ? "ASC" : "DESC";
|
|
280
|
+
};
|
|
250
281
|
if (sort && Object.keys(sort).length > 0) {
|
|
251
282
|
for (const [field, dir] of Object.entries(sort)) {
|
|
252
|
-
|
|
253
|
-
clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
|
|
283
|
+
clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
|
|
254
284
|
directions.push(dir);
|
|
255
285
|
}
|
|
256
286
|
}
|
|
257
287
|
if (!sort || !sort.hasOwnProperty("id")) {
|
|
258
|
-
clauses.push(`${fieldFormatter("id")}
|
|
288
|
+
clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
|
|
259
289
|
directions.push(1);
|
|
260
290
|
} else if (sort && sort.hasOwnProperty("id")) {
|
|
261
291
|
directions.push(sort["id"]);
|
|
@@ -278,18 +308,27 @@ var init_CursorManager = __esm({
|
|
|
278
308
|
/**
|
|
279
309
|
* Generate next and previous cursors from result set
|
|
280
310
|
*/
|
|
281
|
-
static generateResultCursors(results, sortFields,
|
|
311
|
+
static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
|
|
282
312
|
if (results.length === 0) {
|
|
283
313
|
return {};
|
|
284
314
|
}
|
|
285
315
|
const cursors = {};
|
|
286
316
|
if (hasMore && results.length > 0) {
|
|
287
317
|
const lastResult = results[results.length - 1];
|
|
288
|
-
cursors.nextCursor = this.generateCursor(
|
|
318
|
+
cursors.nextCursor = this.generateCursor(
|
|
319
|
+
lastResult,
|
|
320
|
+
sortFields,
|
|
321
|
+
requestedDirection
|
|
322
|
+
);
|
|
289
323
|
}
|
|
290
324
|
if (results.length > 0 && !isFirstPage) {
|
|
291
325
|
const firstResult = results[0];
|
|
292
|
-
|
|
326
|
+
const opposite = requestedDirection === 1 ? -1 : 1;
|
|
327
|
+
cursors.prevCursor = this.generateCursor(
|
|
328
|
+
firstResult,
|
|
329
|
+
sortFields,
|
|
330
|
+
opposite
|
|
331
|
+
);
|
|
293
332
|
}
|
|
294
333
|
return cursors;
|
|
295
334
|
}
|
|
@@ -339,6 +378,22 @@ var init_patterns = __esm({
|
|
|
339
378
|
}
|
|
340
379
|
});
|
|
341
380
|
|
|
381
|
+
// src/utils/yjsValues.ts
|
|
382
|
+
function isStringSetYMap(value) {
|
|
383
|
+
if (!(value instanceof Y.Map)) return false;
|
|
384
|
+
for (const v of value.values()) {
|
|
385
|
+
if (v !== true) return false;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
var Y;
|
|
390
|
+
var init_yjsValues = __esm({
|
|
391
|
+
"src/utils/yjsValues.ts"() {
|
|
392
|
+
"use strict";
|
|
393
|
+
Y = __toESM(require("yjs"), 1);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
342
397
|
// src/models/StringSet.ts
|
|
343
398
|
var init_StringSet = __esm({
|
|
344
399
|
"src/models/StringSet.ts"() {
|
|
@@ -364,9 +419,34 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
364
419
|
}
|
|
365
420
|
});
|
|
366
421
|
|
|
422
|
+
// src/models/writeLifecycle.ts
|
|
423
|
+
function registerFunctionDefaultGenerator(name, fn) {
|
|
424
|
+
FUNCTION_DEFAULT_GENERATORS.set(name, fn);
|
|
425
|
+
}
|
|
426
|
+
var Y3, import_ulid2, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
|
|
427
|
+
var init_writeLifecycle = __esm({
|
|
428
|
+
"src/models/writeLifecycle.ts"() {
|
|
429
|
+
"use strict";
|
|
430
|
+
Y3 = __toESM(require("yjs"), 1);
|
|
431
|
+
import_ulid2 = require("ulid");
|
|
432
|
+
KNOWN_FIELD_TYPE_MAP = {
|
|
433
|
+
string: true,
|
|
434
|
+
number: true,
|
|
435
|
+
boolean: true,
|
|
436
|
+
date: true,
|
|
437
|
+
id: true,
|
|
438
|
+
stringset: true
|
|
439
|
+
};
|
|
440
|
+
KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
|
|
441
|
+
FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
|
|
442
|
+
registerFunctionDefaultGenerator("generate_ulid", () => (0, import_ulid2.ulid)());
|
|
443
|
+
UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
367
447
|
// src/models/metaSync.ts
|
|
368
448
|
function inferFieldType(value) {
|
|
369
|
-
if (value
|
|
449
|
+
if (isStringSetYMap(value)) return "stringset";
|
|
370
450
|
switch (typeof value) {
|
|
371
451
|
case "string":
|
|
372
452
|
return "string";
|
|
@@ -385,7 +465,10 @@ function encodeDefault(value) {
|
|
|
385
465
|
if (value === void 0 || value === null) return void 0;
|
|
386
466
|
if (typeof value === "function") {
|
|
387
467
|
const name = KNOWN_FUNCTION_DEFAULTS.get(value);
|
|
388
|
-
return name ? `$${name}` :
|
|
468
|
+
return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
|
|
469
|
+
}
|
|
470
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
471
|
+
return `$${value}`;
|
|
389
472
|
}
|
|
390
473
|
return value;
|
|
391
474
|
}
|
|
@@ -411,7 +494,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
411
494
|
if (compoundConstraints.length > 0) {
|
|
412
495
|
let constraints = meta.get("_constraints");
|
|
413
496
|
if (!constraints) {
|
|
414
|
-
constraints = new
|
|
497
|
+
constraints = new Y4.Map();
|
|
415
498
|
meta.set("_constraints", constraints);
|
|
416
499
|
}
|
|
417
500
|
for (const constraint of compoundConstraints) {
|
|
@@ -422,7 +505,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
422
505
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
423
506
|
let rels = meta.get("_relationships");
|
|
424
507
|
if (!rels) {
|
|
425
|
-
rels = new
|
|
508
|
+
rels = new Y4.Map();
|
|
426
509
|
meta.set("_relationships", rels);
|
|
427
510
|
}
|
|
428
511
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
@@ -434,7 +517,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
434
517
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
435
518
|
let fieldMeta = metaMap.get(fieldName);
|
|
436
519
|
if (!fieldMeta) {
|
|
437
|
-
fieldMeta = new
|
|
520
|
+
fieldMeta = new Y4.Map();
|
|
438
521
|
metaMap.set(fieldName, fieldMeta);
|
|
439
522
|
}
|
|
440
523
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -442,6 +525,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
442
525
|
if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
|
|
443
526
|
if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
|
|
444
527
|
if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
|
|
528
|
+
if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
|
|
445
529
|
if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
|
|
446
530
|
if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
|
|
447
531
|
const encoded = encodeDefault(fieldOpts.default);
|
|
@@ -450,7 +534,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
450
534
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
451
535
|
let cMeta = constraintsMap.get(constraint.name);
|
|
452
536
|
if (!cMeta) {
|
|
453
|
-
cMeta = new
|
|
537
|
+
cMeta = new Y4.Map();
|
|
454
538
|
constraintsMap.set(constraint.name, cMeta);
|
|
455
539
|
}
|
|
456
540
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -460,7 +544,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
460
544
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
461
545
|
let relMeta = relsMap.get(relName);
|
|
462
546
|
if (!relMeta) {
|
|
463
|
-
relMeta = new
|
|
547
|
+
relMeta = new Y4.Map();
|
|
464
548
|
relsMap.set(relName, relMeta);
|
|
465
549
|
}
|
|
466
550
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -475,7 +559,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
475
559
|
if (fieldName.startsWith("_")) continue;
|
|
476
560
|
let fieldMeta = meta.get(fieldName);
|
|
477
561
|
if (!fieldMeta) {
|
|
478
|
-
fieldMeta = new
|
|
562
|
+
fieldMeta = new Y4.Map();
|
|
479
563
|
meta.set(fieldName, fieldMeta);
|
|
480
564
|
}
|
|
481
565
|
if (!fieldMeta.has("type")) {
|
|
@@ -491,32 +575,49 @@ function setIfChanged(map, key, value) {
|
|
|
491
575
|
map.set(key, value);
|
|
492
576
|
}
|
|
493
577
|
}
|
|
494
|
-
var
|
|
578
|
+
var Y4, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
|
|
495
579
|
var init_metaSync = __esm({
|
|
496
580
|
"src/models/metaSync.ts"() {
|
|
497
581
|
"use strict";
|
|
498
|
-
|
|
582
|
+
Y4 = __toESM(require("yjs"), 1);
|
|
583
|
+
init_yjsValues();
|
|
584
|
+
init_writeLifecycle();
|
|
499
585
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
500
586
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
501
587
|
}
|
|
502
588
|
});
|
|
503
589
|
|
|
590
|
+
// src/utils/uniqueIndex.ts
|
|
591
|
+
function fieldUniqueConstraintName(modelName, fieldName) {
|
|
592
|
+
return `${modelName}_${fieldName}_unique`;
|
|
593
|
+
}
|
|
594
|
+
var Y5;
|
|
595
|
+
var init_uniqueIndex = __esm({
|
|
596
|
+
"src/utils/uniqueIndex.ts"() {
|
|
597
|
+
"use strict";
|
|
598
|
+
Y5 = __toESM(require("yjs"), 1);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
504
602
|
// src/models/BaseModel.ts
|
|
505
603
|
function generateULID() {
|
|
506
|
-
return (0,
|
|
604
|
+
return (0, import_ulid3.ulid)();
|
|
507
605
|
}
|
|
508
|
-
var
|
|
606
|
+
var Y6, import_ulid3;
|
|
509
607
|
var init_BaseModel = __esm({
|
|
510
608
|
"src/models/BaseModel.ts"() {
|
|
511
609
|
"use strict";
|
|
512
|
-
|
|
513
|
-
|
|
610
|
+
Y6 = __toESM(require("yjs"), 1);
|
|
611
|
+
import_ulid3 = require("ulid");
|
|
514
612
|
init_StringSet();
|
|
515
613
|
init_documentTypes();
|
|
516
614
|
init_DocumentQueryTranslator();
|
|
517
615
|
init_CursorManager();
|
|
518
616
|
init_sql();
|
|
519
617
|
init_metaSync();
|
|
618
|
+
init_writeLifecycle();
|
|
619
|
+
init_yjsValues();
|
|
620
|
+
init_uniqueIndex();
|
|
520
621
|
registerFunctionDefault(generateULID, "generate_ulid");
|
|
521
622
|
}
|
|
522
623
|
});
|
|
@@ -544,14 +645,21 @@ __export(cloudflare_do_exports, {
|
|
|
544
645
|
DurableObjectEngine: () => DurableObjectEngine,
|
|
545
646
|
JsonQueryTranslator: () => JsonQueryTranslator,
|
|
546
647
|
JsonSchemaDDL: () => JsonSchemaDDL,
|
|
648
|
+
PROJECTED_JSON_ALIAS_PREFIX: () => PROJECTED_JSON_ALIAS_PREFIX,
|
|
649
|
+
buildSafeAlias: () => buildSafeAlias,
|
|
650
|
+
checkAutoStampsReservedFields: () => checkAutoStampsReservedFields,
|
|
651
|
+
checkReservedFieldName: () => checkReservedFieldName,
|
|
547
652
|
clearMetaSyncCache: () => clearMetaSyncCache,
|
|
548
653
|
createDatabaseDO: () => createDatabaseDO,
|
|
549
654
|
createDocumentDO: () => createDocumentDO,
|
|
655
|
+
deterministicIdentifierHash: () => deterministicIdentifierHash,
|
|
550
656
|
discoverModelNames: () => discoverModelNames,
|
|
551
657
|
discoverSchema: () => discoverSchema,
|
|
552
658
|
handleRequest: () => handleRequest,
|
|
553
659
|
inferFieldType: () => inferFieldType,
|
|
554
660
|
loadSchemaFromTomlString: () => loadSchemaFromTomlString,
|
|
661
|
+
parseRecordRow: () => parseRecordRow,
|
|
662
|
+
projectedJsonAlias: () => projectedJsonAlias,
|
|
555
663
|
registerFunctionDefault: () => registerFunctionDefault,
|
|
556
664
|
schemaToToml: () => schemaToToml,
|
|
557
665
|
syncInferredMeta: () => syncInferredMeta,
|
|
@@ -1567,6 +1675,45 @@ var DurableObjectEngine = class extends JsonSchemaEngine {
|
|
|
1567
1675
|
init_queryTypes();
|
|
1568
1676
|
init_CursorManager();
|
|
1569
1677
|
init_sql();
|
|
1678
|
+
|
|
1679
|
+
// src/query/recordRow.ts
|
|
1680
|
+
var PROJECTED_JSON_ALIAS_PREFIX = "_json_";
|
|
1681
|
+
function projectedJsonAlias(fieldName) {
|
|
1682
|
+
return `${PROJECTED_JSON_ALIAS_PREFIX}${fieldName}`;
|
|
1683
|
+
}
|
|
1684
|
+
function parseRecordRow(row) {
|
|
1685
|
+
const { _id, _type, _data, ...rest } = row;
|
|
1686
|
+
let parsed = {};
|
|
1687
|
+
if (_data) {
|
|
1688
|
+
try {
|
|
1689
|
+
parsed = JSON.parse(_data);
|
|
1690
|
+
} catch (e) {
|
|
1691
|
+
console.warn("Failed to parse _data:", e);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
return { id: _id, type: _type, ...parsed, ...decodeProjectedColumns(rest) };
|
|
1695
|
+
}
|
|
1696
|
+
function decodeProjectedColumns(columns) {
|
|
1697
|
+
const decoded = {};
|
|
1698
|
+
for (const [column, value] of Object.entries(columns)) {
|
|
1699
|
+
if (!column.startsWith(PROJECTED_JSON_ALIAS_PREFIX)) {
|
|
1700
|
+
decoded[column] = value;
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
decoded[column.slice(PROJECTED_JSON_ALIAS_PREFIX.length)] = parseJsonColumn(value);
|
|
1704
|
+
}
|
|
1705
|
+
return decoded;
|
|
1706
|
+
}
|
|
1707
|
+
function parseJsonColumn(value) {
|
|
1708
|
+
if (typeof value !== "string") return value;
|
|
1709
|
+
try {
|
|
1710
|
+
return JSON.parse(value);
|
|
1711
|
+
} catch {
|
|
1712
|
+
return value;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/query/JsonQueryTranslator.ts
|
|
1570
1717
|
init_patterns();
|
|
1571
1718
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
|
|
1572
1719
|
var JsonQueryTranslator = class {
|
|
@@ -1611,7 +1758,8 @@ var JsonQueryTranslator = class {
|
|
|
1611
1758
|
const whereClause = this.translateFilter(filter);
|
|
1612
1759
|
const orderClause = CursorManager.buildOrderClause(
|
|
1613
1760
|
options?.sort,
|
|
1614
|
-
(field) => this.getFieldSql(field)
|
|
1761
|
+
(field) => this.getFieldSql(field),
|
|
1762
|
+
options?.direction || 1
|
|
1615
1763
|
);
|
|
1616
1764
|
const limitClause = this.buildLimitClause(options);
|
|
1617
1765
|
const paginationClause = this.buildPaginationClause(
|
|
@@ -2062,7 +2210,7 @@ var JsonQueryTranslator = class {
|
|
|
2062
2210
|
for (const field of includeFields) {
|
|
2063
2211
|
if (field === "id" || field === "type") continue;
|
|
2064
2212
|
selectParts.push(
|
|
2065
|
-
`
|
|
2213
|
+
`_data -> '$.${field}' AS ${quoteIdentifier(projectedJsonAlias(field))}`
|
|
2066
2214
|
);
|
|
2067
2215
|
}
|
|
2068
2216
|
return selectParts.join(", ");
|
|
@@ -2249,6 +2397,86 @@ var JsonQueryTranslator = class {
|
|
|
2249
2397
|
init_CursorManager();
|
|
2250
2398
|
init_sql();
|
|
2251
2399
|
var import_ulid = require("ulid");
|
|
2400
|
+
var AtomicBatchAbort = class extends Error {
|
|
2401
|
+
constructor(failedIndex) {
|
|
2402
|
+
super(`atomic batch aborted at operation ${failedIndex}`);
|
|
2403
|
+
this.failedIndex = failedIndex;
|
|
2404
|
+
this.name = "AtomicBatchAbort";
|
|
2405
|
+
}
|
|
2406
|
+
};
|
|
2407
|
+
function checkReservedFieldName(fieldName) {
|
|
2408
|
+
if (fieldName === "id") return null;
|
|
2409
|
+
if (fieldName === "type") {
|
|
2410
|
+
return `Field 'type' is reserved (maps to internal _type column in queries)`;
|
|
2411
|
+
}
|
|
2412
|
+
if (fieldName.startsWith("_")) {
|
|
2413
|
+
return `Field '${fieldName}' is reserved (fields starting with '_' are internal)`;
|
|
2414
|
+
}
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
function checkAutoStampsReservedFields(autoStamps) {
|
|
2418
|
+
if (!autoStamps) return null;
|
|
2419
|
+
for (const field of autoStamps.create || []) {
|
|
2420
|
+
const err = checkReservedFieldName(field);
|
|
2421
|
+
if (err) return `autoStamps.create: ${err}`;
|
|
2422
|
+
}
|
|
2423
|
+
for (const field of autoStamps.update || []) {
|
|
2424
|
+
const err = checkReservedFieldName(field);
|
|
2425
|
+
if (err) return `autoStamps.update: ${err}`;
|
|
2426
|
+
}
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
function applyAutoStamps(data, autoStamps, isNew) {
|
|
2430
|
+
if (!autoStamps || !data) return;
|
|
2431
|
+
const reservedError = checkAutoStampsReservedFields(autoStamps);
|
|
2432
|
+
if (reservedError) {
|
|
2433
|
+
throw new Error(reservedError);
|
|
2434
|
+
}
|
|
2435
|
+
const now = Date.now();
|
|
2436
|
+
if (isNew && autoStamps.create) {
|
|
2437
|
+
for (const field of autoStamps.create) {
|
|
2438
|
+
if (data[field] === void 0 || data[field] === null) {
|
|
2439
|
+
data[field] = now;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
if (autoStamps.update) {
|
|
2444
|
+
for (const field of autoStamps.update) {
|
|
2445
|
+
if (data[field] === void 0 || data[field] === null) {
|
|
2446
|
+
data[field] = now;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function collectAppliedStamps(data, autoStamps, isNew) {
|
|
2452
|
+
if (!autoStamps || !data) return void 0;
|
|
2453
|
+
const stamps = {};
|
|
2454
|
+
const take = (fields) => {
|
|
2455
|
+
if (!fields) return;
|
|
2456
|
+
for (const field of fields) {
|
|
2457
|
+
const value = data[field];
|
|
2458
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2459
|
+
stamps[field] = value;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
if (isNew) take(autoStamps.create);
|
|
2464
|
+
take(autoStamps.update);
|
|
2465
|
+
return Object.keys(stamps).length > 0 ? stamps : void 0;
|
|
2466
|
+
}
|
|
2467
|
+
function collectAppliedValues(data, fieldNames) {
|
|
2468
|
+
if (!data || !fieldNames || fieldNames.length === 0) return void 0;
|
|
2469
|
+
const out = {};
|
|
2470
|
+
for (const name of fieldNames) {
|
|
2471
|
+
const value = data[name];
|
|
2472
|
+
if (value !== void 0) out[name] = value;
|
|
2473
|
+
}
|
|
2474
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2475
|
+
}
|
|
2476
|
+
function mergeAppliedFields(stamps, applied) {
|
|
2477
|
+
if (!stamps && !applied) return void 0;
|
|
2478
|
+
return { ...stamps ?? {}, ...applied ?? {} };
|
|
2479
|
+
}
|
|
2252
2480
|
function createDatabaseDO(config = {}) {
|
|
2253
2481
|
const hooks = config.hooks;
|
|
2254
2482
|
return class DocumentDO {
|
|
@@ -2406,6 +2634,16 @@ function createDatabaseDO(config = {}) {
|
|
|
2406
2634
|
hasMore,
|
|
2407
2635
|
isFirstPage
|
|
2408
2636
|
);
|
|
2637
|
+
if (options?.projection && Object.keys(options.projection).length > 0) {
|
|
2638
|
+
const isExclusion = Object.values(options.projection).some(
|
|
2639
|
+
(v) => v === 0
|
|
2640
|
+
);
|
|
2641
|
+
if (isExclusion) {
|
|
2642
|
+
data = data.map(
|
|
2643
|
+
(rec) => this._applyProjection(rec, options.projection)
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2409
2647
|
const response = {
|
|
2410
2648
|
data,
|
|
2411
2649
|
hasMore,
|
|
@@ -2429,7 +2667,15 @@ function createDatabaseDO(config = {}) {
|
|
|
2429
2667
|
/** @internal */
|
|
2430
2668
|
async _handleSave(request, docId) {
|
|
2431
2669
|
const body = await request.json();
|
|
2432
|
-
const {
|
|
2670
|
+
const {
|
|
2671
|
+
modelName,
|
|
2672
|
+
data,
|
|
2673
|
+
stringSets,
|
|
2674
|
+
ifNotExists,
|
|
2675
|
+
condition,
|
|
2676
|
+
upsertOn,
|
|
2677
|
+
autoStamps
|
|
2678
|
+
} = body;
|
|
2433
2679
|
let id = body.id;
|
|
2434
2680
|
if (!modelName || !id && !upsertOn) {
|
|
2435
2681
|
return this._errorResponse("modelName is required, and either id or upsertOn must be provided", 400);
|
|
@@ -2443,6 +2689,12 @@ function createDatabaseDO(config = {}) {
|
|
|
2443
2689
|
return this._errorResponse(reservedError, 400);
|
|
2444
2690
|
}
|
|
2445
2691
|
}
|
|
2692
|
+
{
|
|
2693
|
+
const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
|
|
2694
|
+
if (autoStampsReservedError) {
|
|
2695
|
+
return this._errorResponse(autoStampsReservedError, 400);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2446
2698
|
if (upsertOn) {
|
|
2447
2699
|
if (data[upsertOn] === void 0 || data[upsertOn] === null || data[upsertOn] === "") {
|
|
2448
2700
|
return this._errorResponse(
|
|
@@ -2492,8 +2744,10 @@ function createDatabaseDO(config = {}) {
|
|
|
2492
2744
|
409
|
|
2493
2745
|
);
|
|
2494
2746
|
}
|
|
2747
|
+
const isNew = !this._engine.recordExists(modelName, id);
|
|
2748
|
+
applyAutoStamps(data, autoStamps, isNew);
|
|
2749
|
+
let hookAppliedNames;
|
|
2495
2750
|
if (hooks?.beforeSave) {
|
|
2496
|
-
const isNew = !this._engine.recordExists(modelName, id);
|
|
2497
2751
|
const ctx = {
|
|
2498
2752
|
modelName,
|
|
2499
2753
|
docId,
|
|
@@ -2508,6 +2762,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2508
2762
|
if (!result.allow) {
|
|
2509
2763
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2510
2764
|
}
|
|
2765
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2511
2766
|
}
|
|
2512
2767
|
const existingForUnique = this._engine.execSqlSync(
|
|
2513
2768
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2537,12 +2792,17 @@ function createDatabaseDO(config = {}) {
|
|
|
2537
2792
|
}
|
|
2538
2793
|
this._engine.trackModelFields(modelName, data);
|
|
2539
2794
|
const response = { success: true, id };
|
|
2795
|
+
const appliedFields = mergeAppliedFields(
|
|
2796
|
+
collectAppliedStamps(data, autoStamps, isNew),
|
|
2797
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
2798
|
+
);
|
|
2799
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2540
2800
|
return Response.json(response);
|
|
2541
2801
|
}
|
|
2542
2802
|
/** @internal — Partial update: merge provided fields into existing record */
|
|
2543
2803
|
async _handlePatch(request, docId) {
|
|
2544
2804
|
const body = await request.json();
|
|
2545
|
-
const { modelName, id, data, stringSets, condition } = body;
|
|
2805
|
+
const { modelName, id, data, stringSets, condition, autoStamps } = body;
|
|
2546
2806
|
if (!modelName || !id || !data) {
|
|
2547
2807
|
return this._errorResponse("modelName, id, and data are required", 400);
|
|
2548
2808
|
}
|
|
@@ -2550,6 +2810,12 @@ function createDatabaseDO(config = {}) {
|
|
|
2550
2810
|
if (reservedError) {
|
|
2551
2811
|
return this._errorResponse(reservedError, 400);
|
|
2552
2812
|
}
|
|
2813
|
+
{
|
|
2814
|
+
const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
|
|
2815
|
+
if (autoStampsReservedError) {
|
|
2816
|
+
return this._errorResponse(autoStampsReservedError, 400);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2553
2819
|
if (!this._engine.recordExists(modelName, id)) {
|
|
2554
2820
|
return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
|
|
2555
2821
|
}
|
|
@@ -2559,6 +2825,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2559
2825
|
409
|
|
2560
2826
|
);
|
|
2561
2827
|
}
|
|
2828
|
+
applyAutoStamps(data, autoStamps, false);
|
|
2829
|
+
let hookAppliedNames;
|
|
2562
2830
|
if (hooks?.beforeSave) {
|
|
2563
2831
|
const ctx = {
|
|
2564
2832
|
modelName,
|
|
@@ -2574,6 +2842,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2574
2842
|
if (!result.allow) {
|
|
2575
2843
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2576
2844
|
}
|
|
2845
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2577
2846
|
}
|
|
2578
2847
|
const existing = this._engine.execSqlSync(
|
|
2579
2848
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2597,6 +2866,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2597
2866
|
}
|
|
2598
2867
|
this._engine.trackModelFields(modelName, data);
|
|
2599
2868
|
const response = { success: true, id };
|
|
2869
|
+
const appliedFields = mergeAppliedFields(
|
|
2870
|
+
collectAppliedStamps(data, autoStamps, false),
|
|
2871
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
2872
|
+
);
|
|
2873
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2600
2874
|
return Response.json(response);
|
|
2601
2875
|
}
|
|
2602
2876
|
/** @internal */
|
|
@@ -2638,13 +2912,16 @@ function createDatabaseDO(config = {}) {
|
|
|
2638
2912
|
/** @internal — Execute multiple save/patch/delete operations in a single transaction */
|
|
2639
2913
|
async _handleBatch(request, docId) {
|
|
2640
2914
|
const body = await request.json();
|
|
2641
|
-
const { operations } = body;
|
|
2915
|
+
const { operations, atomic = false } = body;
|
|
2642
2916
|
if (!operations || !Array.isArray(operations) || operations.length === 0) {
|
|
2643
2917
|
return this._errorResponse("operations array is required and must not be empty", 400);
|
|
2644
2918
|
}
|
|
2645
2919
|
const hookDenials = /* @__PURE__ */ new Map();
|
|
2646
2920
|
const resolvedBatchIds = /* @__PURE__ */ new Map();
|
|
2647
2921
|
const opErrors = /* @__PURE__ */ new Map();
|
|
2922
|
+
const batchCreatedIds = /* @__PURE__ */ new Set();
|
|
2923
|
+
const batchIsNew = /* @__PURE__ */ new Map();
|
|
2924
|
+
const batchAppliedNames = /* @__PURE__ */ new Map();
|
|
2648
2925
|
for (let i = 0; i < operations.length; i++) {
|
|
2649
2926
|
const op = operations[i];
|
|
2650
2927
|
if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
|
|
@@ -2696,9 +2973,31 @@ function createDatabaseDO(config = {}) {
|
|
|
2696
2973
|
400
|
|
2697
2974
|
);
|
|
2698
2975
|
}
|
|
2976
|
+
const autoStampsReservedError = checkAutoStampsReservedFields(
|
|
2977
|
+
op.autoStamps
|
|
2978
|
+
);
|
|
2979
|
+
if (autoStampsReservedError) {
|
|
2980
|
+
return this._errorResponse(
|
|
2981
|
+
`Operation ${i}: ${autoStampsReservedError}`,
|
|
2982
|
+
400
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
const effectiveId = resolvedBatchIds.get(i) || op.id || "";
|
|
2986
|
+
const isNew = op.op === "save" ? (
|
|
2987
|
+
// Codex P2 #3: also treat the id as not-new if a previous
|
|
2988
|
+
// op in this batch already resolved to a save for the same
|
|
2989
|
+
// model+id. Without this, two saves to the same fresh id
|
|
2990
|
+
// both compute isNew=true against the pre-transaction
|
|
2991
|
+
// database, so a `'create'` autoStamp on the second op
|
|
2992
|
+
// would overwrite the first op's stamp.
|
|
2993
|
+
!batchCreatedIds.has(`${op.modelName}/${effectiveId}`) && !this._engine.recordExists(op.modelName, effectiveId)
|
|
2994
|
+
) : false;
|
|
2995
|
+
if (op.op === "save" && isNew && effectiveId) {
|
|
2996
|
+
batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
|
|
2997
|
+
}
|
|
2998
|
+
batchIsNew.set(i, isNew);
|
|
2999
|
+
applyAutoStamps(op.data, op.autoStamps, isNew);
|
|
2699
3000
|
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
3001
|
const ctx = {
|
|
2703
3002
|
modelName: op.modelName,
|
|
2704
3003
|
docId,
|
|
@@ -2712,6 +3011,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2712
3011
|
const result = await hooks.beforeSave(ctx);
|
|
2713
3012
|
if (!result.allow) {
|
|
2714
3013
|
hookDenials.set(i, result.reason || "Save denied");
|
|
3014
|
+
} else if (result.serverAppliedFieldNames) {
|
|
3015
|
+
batchAppliedNames.set(i, result.serverAppliedFieldNames);
|
|
2715
3016
|
}
|
|
2716
3017
|
}
|
|
2717
3018
|
} else if (op.op === "delete") {
|
|
@@ -2760,205 +3061,263 @@ function createDatabaseDO(config = {}) {
|
|
|
2760
3061
|
}
|
|
2761
3062
|
const results = [];
|
|
2762
3063
|
const fieldsToTrack = [];
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
2785
|
-
results.push({
|
|
2786
|
-
success: false,
|
|
2787
|
-
id: conditionId,
|
|
2788
|
-
error: `Condition not met for ${op.modelName}/${conditionId}`
|
|
2789
|
-
});
|
|
2790
|
-
continue;
|
|
2791
|
-
}
|
|
2792
|
-
if (op.op === "save") {
|
|
2793
|
-
let opId = resolvedBatchIds.get(i) || op.id;
|
|
2794
|
-
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
2795
|
-
results.push({
|
|
2796
|
-
success: false,
|
|
2797
|
-
id: opId,
|
|
2798
|
-
error: `Record ${op.modelName}/${opId} already exists`
|
|
3064
|
+
const pushFailure = (index, failure) => {
|
|
3065
|
+
results.push({
|
|
3066
|
+
success: false,
|
|
3067
|
+
id: failure.id,
|
|
3068
|
+
error: failure.error,
|
|
3069
|
+
status: failure.status,
|
|
3070
|
+
code: failure.code
|
|
3071
|
+
});
|
|
3072
|
+
if (atomic) throw new AtomicBatchAbort(index);
|
|
3073
|
+
};
|
|
3074
|
+
try {
|
|
3075
|
+
this._engine.transactionSync(() => {
|
|
3076
|
+
for (let i = 0; i < operations.length; i++) {
|
|
3077
|
+
const op = operations[i];
|
|
3078
|
+
if (hookDenials.has(i)) {
|
|
3079
|
+
const effectiveId = resolvedBatchIds.get(i) || op.id || "";
|
|
3080
|
+
pushFailure(i, {
|
|
3081
|
+
id: effectiveId,
|
|
3082
|
+
error: hookDenials.get(i),
|
|
3083
|
+
status: 403,
|
|
3084
|
+
code: "HOOK_DENIED"
|
|
2799
3085
|
});
|
|
2800
3086
|
continue;
|
|
2801
3087
|
}
|
|
2802
|
-
if (
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3088
|
+
if (opErrors.has(i)) {
|
|
3089
|
+
pushFailure(i, {
|
|
3090
|
+
id: op.id || "",
|
|
3091
|
+
error: opErrors.get(i),
|
|
3092
|
+
status: 400,
|
|
3093
|
+
code: "VALIDATION_ERROR"
|
|
3094
|
+
});
|
|
3095
|
+
continue;
|
|
3096
|
+
}
|
|
3097
|
+
const conditionId = resolvedBatchIds.get(i) || op.id;
|
|
3098
|
+
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
3099
|
+
pushFailure(i, {
|
|
3100
|
+
id: conditionId,
|
|
3101
|
+
error: `Condition not met for ${op.modelName}/${conditionId}`,
|
|
3102
|
+
status: 409,
|
|
3103
|
+
code: "CONDITION_NOT_MET"
|
|
3104
|
+
});
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (op.op === "save") {
|
|
3108
|
+
let opId = resolvedBatchIds.get(i) || op.id;
|
|
3109
|
+
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
3110
|
+
pushFailure(i, {
|
|
2820
3111
|
id: opId,
|
|
2821
|
-
error:
|
|
3112
|
+
error: `Record ${op.modelName}/${opId} already exists`,
|
|
3113
|
+
status: 409,
|
|
3114
|
+
code: "ALREADY_EXISTS"
|
|
2822
3115
|
});
|
|
2823
3116
|
continue;
|
|
2824
3117
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
const dataJson = JSON.stringify(jsonFields);
|
|
2830
|
-
this._engine.execSqlSync(
|
|
2831
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2832
|
-
[opId, op.modelName, dataJson]
|
|
2833
|
-
);
|
|
2834
|
-
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
2835
|
-
this._engine.execSqlSync(
|
|
2836
|
-
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
2837
|
-
[op.modelName, opId, fieldName]
|
|
3118
|
+
if (op.data) {
|
|
3119
|
+
const existingForUnique = this._engine.execSqlSync(
|
|
3120
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3121
|
+
[opId, op.modelName]
|
|
2838
3122
|
);
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
);
|
|
3123
|
+
let dataForUnique = op.data;
|
|
3124
|
+
if (existingForUnique.length > 0) {
|
|
3125
|
+
const existingData = JSON.parse(existingForUnique[0]._data);
|
|
3126
|
+
dataForUnique = { ...existingData, ...op.data };
|
|
2844
3127
|
}
|
|
2845
|
-
}
|
|
2846
|
-
} else {
|
|
2847
|
-
const { id: _id, ...fieldsForJson } = op.data;
|
|
2848
|
-
const dataJson = JSON.stringify(fieldsForJson);
|
|
2849
|
-
this._engine.execSqlSync(
|
|
2850
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2851
|
-
[opId, op.modelName, dataJson]
|
|
2852
|
-
);
|
|
2853
|
-
}
|
|
2854
|
-
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
2855
|
-
results.push({ success: true, id: opId });
|
|
2856
|
-
} else if (op.op === "patch") {
|
|
2857
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
2858
|
-
results.push({
|
|
2859
|
-
success: false,
|
|
2860
|
-
id: op.id,
|
|
2861
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2862
|
-
});
|
|
2863
|
-
continue;
|
|
2864
|
-
}
|
|
2865
|
-
if (op.data) {
|
|
2866
|
-
const existing = this._engine.execSqlSync(
|
|
2867
|
-
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
2868
|
-
[op.id, op.modelName]
|
|
2869
|
-
);
|
|
2870
|
-
if (existing.length > 0) {
|
|
2871
|
-
const existingData = JSON.parse(existing[0]._data);
|
|
2872
|
-
const mergedData = { ...existingData, ...op.data };
|
|
2873
3128
|
const violation = this._engine.checkUniqueConstraints(
|
|
2874
3129
|
op.modelName,
|
|
2875
|
-
|
|
2876
|
-
|
|
3130
|
+
opId,
|
|
3131
|
+
dataForUnique
|
|
2877
3132
|
);
|
|
2878
3133
|
if (violation) {
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3134
|
+
pushFailure(i, {
|
|
3135
|
+
id: opId,
|
|
3136
|
+
error: violation,
|
|
3137
|
+
status: 409,
|
|
3138
|
+
code: "UNIQUE_VIOLATION"
|
|
2883
3139
|
});
|
|
2884
3140
|
continue;
|
|
2885
3141
|
}
|
|
2886
3142
|
}
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3143
|
+
if (op.stringSets && Object.keys(op.stringSets).length > 0) {
|
|
3144
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3145
|
+
const jsonFields = { ...fieldsForJson, ...op.stringSets };
|
|
3146
|
+
const dataJson = JSON.stringify(jsonFields);
|
|
3147
|
+
this._engine.execSqlSync(
|
|
3148
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3149
|
+
[opId, op.modelName, dataJson]
|
|
3150
|
+
);
|
|
3151
|
+
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
3152
|
+
this._engine.execSqlSync(
|
|
3153
|
+
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
3154
|
+
[op.modelName, opId, fieldName]
|
|
3155
|
+
);
|
|
3156
|
+
for (const value of values) {
|
|
3157
|
+
this._engine.execSqlSync(
|
|
3158
|
+
"INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
|
|
3159
|
+
[opId, op.modelName, fieldName, value]
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
} else {
|
|
3164
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3165
|
+
const dataJson = JSON.stringify(fieldsForJson);
|
|
3166
|
+
this._engine.execSqlSync(
|
|
3167
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3168
|
+
[opId, op.modelName, dataJson]
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
2901
3171
|
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3172
|
+
const saveResult = { success: true, id: opId };
|
|
3173
|
+
const saveApplied = mergeAppliedFields(
|
|
3174
|
+
collectAppliedStamps(
|
|
3175
|
+
op.data,
|
|
3176
|
+
op.autoStamps,
|
|
3177
|
+
batchIsNew.get(i) ?? false
|
|
3178
|
+
),
|
|
3179
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3180
|
+
);
|
|
3181
|
+
if (saveApplied) saveResult.appliedFields = saveApplied;
|
|
3182
|
+
results.push(saveResult);
|
|
3183
|
+
} else if (op.op === "patch") {
|
|
3184
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3185
|
+
pushFailure(i, {
|
|
3186
|
+
id: op.id,
|
|
3187
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3188
|
+
status: 404,
|
|
3189
|
+
code: "NOT_FOUND"
|
|
3190
|
+
});
|
|
3191
|
+
continue;
|
|
3192
|
+
}
|
|
3193
|
+
if (op.data) {
|
|
3194
|
+
const existing = this._engine.execSqlSync(
|
|
3195
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3196
|
+
[op.id, op.modelName]
|
|
3197
|
+
);
|
|
3198
|
+
if (existing.length > 0) {
|
|
3199
|
+
const existingData = JSON.parse(existing[0]._data);
|
|
3200
|
+
const mergedData = { ...existingData, ...op.data };
|
|
3201
|
+
const violation = this._engine.checkUniqueConstraints(
|
|
3202
|
+
op.modelName,
|
|
3203
|
+
op.id,
|
|
3204
|
+
mergedData
|
|
3205
|
+
);
|
|
3206
|
+
if (violation) {
|
|
3207
|
+
pushFailure(i, {
|
|
3208
|
+
id: op.id,
|
|
3209
|
+
error: violation,
|
|
3210
|
+
status: 409,
|
|
3211
|
+
code: "UNIQUE_VIOLATION"
|
|
3212
|
+
});
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
const found = this._engine.patchRecordRaw(
|
|
3218
|
+
op.modelName,
|
|
3219
|
+
op.id,
|
|
3220
|
+
op.data,
|
|
3221
|
+
op.stringSets
|
|
3222
|
+
);
|
|
3223
|
+
if (!found) {
|
|
3224
|
+
pushFailure(i, {
|
|
3225
|
+
id: op.id,
|
|
3226
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3227
|
+
status: 404,
|
|
3228
|
+
code: "NOT_FOUND"
|
|
3229
|
+
});
|
|
3230
|
+
} else {
|
|
3231
|
+
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3232
|
+
const patchResult = {
|
|
3233
|
+
success: true,
|
|
3234
|
+
id: op.id
|
|
3235
|
+
};
|
|
3236
|
+
const patchApplied = mergeAppliedFields(
|
|
3237
|
+
collectAppliedStamps(op.data, op.autoStamps, false),
|
|
3238
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3239
|
+
);
|
|
3240
|
+
if (patchApplied) patchResult.appliedFields = patchApplied;
|
|
3241
|
+
results.push(patchResult);
|
|
3242
|
+
}
|
|
3243
|
+
} else if (op.op === "delete") {
|
|
3244
|
+
this._engine.execSqlSync(
|
|
3245
|
+
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
3246
|
+
[op.id, op.modelName]
|
|
3247
|
+
);
|
|
3248
|
+
this._engine.execSqlSync(
|
|
3249
|
+
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
3250
|
+
[op.id, op.modelName]
|
|
3251
|
+
);
|
|
3252
|
+
results.push({ success: true, id: op.id });
|
|
3253
|
+
} else if (op.op === "increment") {
|
|
3254
|
+
const newValues = this._engine.incrementFieldsRaw(
|
|
3255
|
+
op.modelName,
|
|
3256
|
+
op.id,
|
|
3257
|
+
op.fields
|
|
3258
|
+
);
|
|
3259
|
+
if (!newValues) {
|
|
3260
|
+
pushFailure(i, {
|
|
3261
|
+
id: op.id,
|
|
3262
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3263
|
+
status: 404,
|
|
3264
|
+
code: "NOT_FOUND"
|
|
3265
|
+
});
|
|
3266
|
+
} else {
|
|
3267
|
+
results.push({ success: true, id: op.id, values: newValues });
|
|
3268
|
+
}
|
|
3269
|
+
} else if (op.op === "addToSet") {
|
|
3270
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3271
|
+
pushFailure(i, {
|
|
3272
|
+
id: op.id,
|
|
3273
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3274
|
+
status: 404,
|
|
3275
|
+
code: "NOT_FOUND"
|
|
3276
|
+
});
|
|
3277
|
+
continue;
|
|
3278
|
+
}
|
|
3279
|
+
this._engine.addToStringSetsRaw(
|
|
3280
|
+
op.modelName,
|
|
3281
|
+
op.id,
|
|
3282
|
+
op.stringSets
|
|
3283
|
+
);
|
|
3284
|
+
results.push({ success: true, id: op.id });
|
|
3285
|
+
} else if (op.op === "removeFromSet") {
|
|
3286
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3287
|
+
pushFailure(i, {
|
|
3288
|
+
id: op.id,
|
|
3289
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3290
|
+
status: 404,
|
|
3291
|
+
code: "NOT_FOUND"
|
|
3292
|
+
});
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
this._engine.removeFromStringSetsRaw(
|
|
3296
|
+
op.modelName,
|
|
3297
|
+
op.id,
|
|
3298
|
+
op.stringSets
|
|
3299
|
+
);
|
|
2902
3300
|
results.push({ success: true, id: op.id });
|
|
2903
3301
|
}
|
|
2904
|
-
} else if (op.op === "delete") {
|
|
2905
|
-
this._engine.execSqlSync(
|
|
2906
|
-
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
2907
|
-
[op.id, op.modelName]
|
|
2908
|
-
);
|
|
2909
|
-
this._engine.execSqlSync(
|
|
2910
|
-
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
2911
|
-
[op.id, op.modelName]
|
|
2912
|
-
);
|
|
2913
|
-
results.push({ success: true, id: op.id });
|
|
2914
|
-
} else if (op.op === "increment") {
|
|
2915
|
-
const newValues = this._engine.incrementFieldsRaw(
|
|
2916
|
-
op.modelName,
|
|
2917
|
-
op.id,
|
|
2918
|
-
op.fields
|
|
2919
|
-
);
|
|
2920
|
-
if (!newValues) {
|
|
2921
|
-
results.push({
|
|
2922
|
-
success: false,
|
|
2923
|
-
id: op.id,
|
|
2924
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2925
|
-
});
|
|
2926
|
-
} else {
|
|
2927
|
-
results.push({ success: true, id: op.id, values: newValues });
|
|
2928
|
-
}
|
|
2929
|
-
} else if (op.op === "addToSet") {
|
|
2930
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
2931
|
-
results.push({
|
|
2932
|
-
success: false,
|
|
2933
|
-
id: op.id,
|
|
2934
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2935
|
-
});
|
|
2936
|
-
continue;
|
|
2937
|
-
}
|
|
2938
|
-
this._engine.addToStringSetsRaw(
|
|
2939
|
-
op.modelName,
|
|
2940
|
-
op.id,
|
|
2941
|
-
op.stringSets
|
|
2942
|
-
);
|
|
2943
|
-
results.push({ success: true, id: op.id });
|
|
2944
|
-
} else if (op.op === "removeFromSet") {
|
|
2945
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
2946
|
-
results.push({
|
|
2947
|
-
success: false,
|
|
2948
|
-
id: op.id,
|
|
2949
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2950
|
-
});
|
|
2951
|
-
continue;
|
|
2952
|
-
}
|
|
2953
|
-
this._engine.removeFromStringSetsRaw(
|
|
2954
|
-
op.modelName,
|
|
2955
|
-
op.id,
|
|
2956
|
-
op.stringSets
|
|
2957
|
-
);
|
|
2958
|
-
results.push({ success: true, id: op.id });
|
|
2959
3302
|
}
|
|
3303
|
+
});
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
if (err instanceof AtomicBatchAbort) {
|
|
3306
|
+
const failed = results[err.failedIndex];
|
|
3307
|
+
const failedOp = operations[err.failedIndex];
|
|
3308
|
+
console.warn(
|
|
3309
|
+
`[js-bao batch] atomic rollback: operation ${err.failedIndex} (${failedOp?.op}) failed \u2014 ${failed?.error ?? "unknown error"}`
|
|
3310
|
+
);
|
|
3311
|
+
const errorBody = {
|
|
3312
|
+
error: `Operation ${err.failedIndex} (${failedOp?.op}) failed: ${failed?.error ?? "unknown error"}`,
|
|
3313
|
+
failedIndex: err.failedIndex,
|
|
3314
|
+
op: failedOp?.op,
|
|
3315
|
+
code: failed?.code
|
|
3316
|
+
};
|
|
3317
|
+
return Response.json(errorBody, { status: failed?.status ?? 409 });
|
|
2960
3318
|
}
|
|
2961
|
-
|
|
3319
|
+
throw err;
|
|
3320
|
+
}
|
|
2962
3321
|
for (const entry of fieldsToTrack) {
|
|
2963
3322
|
this._engine.trackModelFields(entry.modelName, entry.data);
|
|
2964
3323
|
}
|
|
@@ -3328,6 +3687,18 @@ function createDatabaseDO(config = {}) {
|
|
|
3328
3687
|
}
|
|
3329
3688
|
return facetResult;
|
|
3330
3689
|
}
|
|
3690
|
+
if (aggOptions.groupBy.length === 0) {
|
|
3691
|
+
const row = results[0];
|
|
3692
|
+
const flat = {};
|
|
3693
|
+
for (const op of aggOptions.operations) {
|
|
3694
|
+
if (op.type === "count") {
|
|
3695
|
+
flat.count = row.count;
|
|
3696
|
+
} else {
|
|
3697
|
+
flat[`${op.type}_${op.field}`] = row[`${op.type}_${op.field}`];
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return flat;
|
|
3701
|
+
}
|
|
3331
3702
|
const nestedResult = {};
|
|
3332
3703
|
const aliasLookup = /* @__PURE__ */ new Map();
|
|
3333
3704
|
for (const entry of aliasMap) {
|
|
@@ -3655,16 +4026,7 @@ function createDatabaseDO(config = {}) {
|
|
|
3655
4026
|
}
|
|
3656
4027
|
/** @internal */
|
|
3657
4028
|
_parseRow(row) {
|
|
3658
|
-
|
|
3659
|
-
let parsed = {};
|
|
3660
|
-
if (_data) {
|
|
3661
|
-
try {
|
|
3662
|
-
parsed = JSON.parse(_data);
|
|
3663
|
-
} catch (e) {
|
|
3664
|
-
console.warn("Failed to parse _data:", e);
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
|
-
return { id: _id, type: _type, ...parsed, ...rest };
|
|
4029
|
+
return parseRecordRow(row);
|
|
3668
4030
|
}
|
|
3669
4031
|
// ─── Include (Related Data Loading) ──────────────────────────────
|
|
3670
4032
|
/** @internal — Validate an IncludeSpec, returning an error message or null */
|
|
@@ -3794,8 +4156,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3794
4156
|
spec.filter,
|
|
3795
4157
|
{ projection: spec.projection }
|
|
3796
4158
|
);
|
|
4159
|
+
const relatedProjected = spec.projection ? related.map((r) => this._applyProjection(r, spec.projection)) : related;
|
|
3797
4160
|
const lookupMap = /* @__PURE__ */ new Map();
|
|
3798
|
-
for (const r of
|
|
4161
|
+
for (const r of relatedProjected) {
|
|
3799
4162
|
lookupMap.set(r.id, r);
|
|
3800
4163
|
}
|
|
3801
4164
|
for (const rec of records) {
|
|
@@ -3828,8 +4191,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3828
4191
|
spec.filter,
|
|
3829
4192
|
{ projection: spec.projection }
|
|
3830
4193
|
);
|
|
4194
|
+
const targetsProjected = spec.projection ? targets.map((t) => this._applyProjection(t, spec.projection)) : targets;
|
|
3831
4195
|
const targetLookup = /* @__PURE__ */ new Map();
|
|
3832
|
-
for (const t of
|
|
4196
|
+
for (const t of targetsProjected) {
|
|
3833
4197
|
targetLookup.set(t.id, t);
|
|
3834
4198
|
}
|
|
3835
4199
|
for (const rec of records) {
|
|
@@ -3973,6 +4337,14 @@ function createDatabaseDO(config = {}) {
|
|
|
3973
4337
|
return allResults;
|
|
3974
4338
|
}
|
|
3975
4339
|
/** @internal — Apply projection to a single record */
|
|
4340
|
+
// Apply a projection to a single record. Assumes the projection has
|
|
4341
|
+
// already been validated upstream (JsonQueryTranslator.validateProjection /
|
|
4342
|
+
// buildSelectClause reject mixed inclusion+exclusion): `isInclusion` treats
|
|
4343
|
+
// "any value === 1" as inclusion mode, so an unvalidated mixed projection
|
|
4344
|
+
// would silently ignore the excludes. `id` and `type` are system-identity
|
|
4345
|
+
// fields and are always kept in BOTH modes (a record without its `id` is
|
|
4346
|
+
// unusable downstream). `_meta` is NOT forced — an explicit `{ _meta: 0 }`
|
|
4347
|
+
// is honored.
|
|
3976
4348
|
_applyProjection(record, projection) {
|
|
3977
4349
|
const fields = Object.entries(projection);
|
|
3978
4350
|
if (fields.length === 0) return record;
|
|
@@ -3991,7 +4363,7 @@ function createDatabaseDO(config = {}) {
|
|
|
3991
4363
|
} else {
|
|
3992
4364
|
const result = { ...record };
|
|
3993
4365
|
for (const [f, v] of fields) {
|
|
3994
|
-
if (v === 0) delete result[f];
|
|
4366
|
+
if (v === 0 && f !== "id" && f !== "type") delete result[f];
|
|
3995
4367
|
}
|
|
3996
4368
|
return result;
|
|
3997
4369
|
}
|
|
@@ -4059,7 +4431,8 @@ async function handleRequest(request, env) {
|
|
|
4059
4431
|
}
|
|
4060
4432
|
|
|
4061
4433
|
// src/utils/yDocSchema.ts
|
|
4062
|
-
var
|
|
4434
|
+
var Y2 = __toESM(require("yjs"), 1);
|
|
4435
|
+
init_yjsValues();
|
|
4063
4436
|
function discoverSchema(yDoc) {
|
|
4064
4437
|
const models = {};
|
|
4065
4438
|
const metaNames = /* @__PURE__ */ new Set();
|
|
@@ -4093,7 +4466,7 @@ function discoverModelNames(yDoc) {
|
|
|
4093
4466
|
function materializeMap(yDoc, key) {
|
|
4094
4467
|
try {
|
|
4095
4468
|
const map = yDoc.getMap(key);
|
|
4096
|
-
return map instanceof
|
|
4469
|
+
return map instanceof Y2.Map ? map : null;
|
|
4097
4470
|
} catch {
|
|
4098
4471
|
return null;
|
|
4099
4472
|
}
|
|
@@ -4103,11 +4476,11 @@ function readModelMeta(metaMap) {
|
|
|
4103
4476
|
let constraints;
|
|
4104
4477
|
let relationships;
|
|
4105
4478
|
for (const [key, value] of metaMap.entries()) {
|
|
4106
|
-
if (key === "_constraints" && value instanceof
|
|
4479
|
+
if (key === "_constraints" && value instanceof Y2.Map) {
|
|
4107
4480
|
constraints = readConstraints(value);
|
|
4108
|
-
} else if (key === "_relationships" && value instanceof
|
|
4481
|
+
} else if (key === "_relationships" && value instanceof Y2.Map) {
|
|
4109
4482
|
relationships = readRelationships(value);
|
|
4110
|
-
} else if (value instanceof
|
|
4483
|
+
} else if (value instanceof Y2.Map) {
|
|
4111
4484
|
fields[key] = readFieldMeta(value);
|
|
4112
4485
|
}
|
|
4113
4486
|
}
|
|
@@ -4126,6 +4499,10 @@ function readFieldMeta(fieldMap) {
|
|
|
4126
4499
|
if (fieldMap.get("unique") === true) field.unique = true;
|
|
4127
4500
|
if (fieldMap.get("required") === true) field.required = true;
|
|
4128
4501
|
if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
|
|
4502
|
+
const autoStamp = fieldMap.get("autoStamp");
|
|
4503
|
+
if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
|
|
4504
|
+
field.autoStamp = autoStamp;
|
|
4505
|
+
}
|
|
4129
4506
|
const def = fieldMap.get("default");
|
|
4130
4507
|
if (def !== void 0) field.default = def;
|
|
4131
4508
|
const maxLength = fieldMap.get("maxLength");
|
|
@@ -4138,7 +4515,7 @@ function inferModelFromData(dataMap) {
|
|
|
4138
4515
|
const fields = {};
|
|
4139
4516
|
let sampled = 0;
|
|
4140
4517
|
for (const [_recordId, recordValue] of dataMap.entries()) {
|
|
4141
|
-
if (!(recordValue instanceof
|
|
4518
|
+
if (!(recordValue instanceof Y2.Map)) continue;
|
|
4142
4519
|
if (++sampled > 5) break;
|
|
4143
4520
|
for (const [fieldName, value] of recordValue.entries()) {
|
|
4144
4521
|
if (fieldName.startsWith("_")) continue;
|
|
@@ -4151,7 +4528,7 @@ function inferModelFromData(dataMap) {
|
|
|
4151
4528
|
return { fields };
|
|
4152
4529
|
}
|
|
4153
4530
|
function inferTypeFromValue(value) {
|
|
4154
|
-
if (value
|
|
4531
|
+
if (isStringSetYMap(value)) return "stringset";
|
|
4155
4532
|
switch (typeof value) {
|
|
4156
4533
|
case "string":
|
|
4157
4534
|
return "string";
|
|
@@ -4166,7 +4543,7 @@ function inferTypeFromValue(value) {
|
|
|
4166
4543
|
function readConstraints(constraintsMap) {
|
|
4167
4544
|
const out = {};
|
|
4168
4545
|
for (const [name, value] of constraintsMap.entries()) {
|
|
4169
|
-
if (!(value instanceof
|
|
4546
|
+
if (!(value instanceof Y2.Map)) continue;
|
|
4170
4547
|
let fields = [];
|
|
4171
4548
|
const rawFields = value.get("fields");
|
|
4172
4549
|
if (typeof rawFields === "string") {
|
|
@@ -4185,7 +4562,7 @@ function readConstraints(constraintsMap) {
|
|
|
4185
4562
|
function readRelationships(relsMap) {
|
|
4186
4563
|
const out = {};
|
|
4187
4564
|
for (const [name, value] of relsMap.entries()) {
|
|
4188
|
-
if (!(value instanceof
|
|
4565
|
+
if (!(value instanceof Y2.Map)) continue;
|
|
4189
4566
|
const rel = {};
|
|
4190
4567
|
for (const [k, v] of value.entries()) {
|
|
4191
4568
|
rel[k] = v;
|
|
@@ -4196,6 +4573,7 @@ function readRelationships(relsMap) {
|
|
|
4196
4573
|
}
|
|
4197
4574
|
var CAMEL_TO_SNAKE = {
|
|
4198
4575
|
autoAssign: "auto_assign",
|
|
4576
|
+
autoStamp: "auto_stamp",
|
|
4199
4577
|
maxLength: "max_length",
|
|
4200
4578
|
maxCount: "max_count",
|
|
4201
4579
|
relatedIdField: "related_id_field",
|
|
@@ -4226,6 +4604,7 @@ function schemaToToml(schema) {
|
|
|
4226
4604
|
lines.push(`[models.${modelName}.fields.${fieldName}]`);
|
|
4227
4605
|
lines.push(`type = ${tomlValue(field.type)}`);
|
|
4228
4606
|
if (field.autoAssign) lines.push("auto_assign = true");
|
|
4607
|
+
if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
|
|
4229
4608
|
if (field.indexed) lines.push("indexed = true");
|
|
4230
4609
|
if (field.unique) lines.push("unique = true");
|
|
4231
4610
|
if (field.required) lines.push("required = true");
|
|
@@ -4262,6 +4641,7 @@ var import_smol_toml = require("smol-toml");
|
|
|
4262
4641
|
// src/models/schema.ts
|
|
4263
4642
|
init_BaseModel();
|
|
4264
4643
|
init_ModelRegistry();
|
|
4644
|
+
init_uniqueIndex();
|
|
4265
4645
|
function defineModelSchema(input) {
|
|
4266
4646
|
const { name, fields } = input;
|
|
4267
4647
|
const options = {
|
|
@@ -4304,7 +4684,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
|
4304
4684
|
for (const [fieldName, options] of fields.entries()) {
|
|
4305
4685
|
if (options.unique) {
|
|
4306
4686
|
resolved.push({
|
|
4307
|
-
name:
|
|
4687
|
+
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
4308
4688
|
fields: [fieldName]
|
|
4309
4689
|
});
|
|
4310
4690
|
}
|
|
@@ -4349,10 +4729,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
|
4349
4729
|
"unique",
|
|
4350
4730
|
"required",
|
|
4351
4731
|
"auto_assign",
|
|
4732
|
+
"auto_stamp",
|
|
4352
4733
|
"max_length",
|
|
4353
4734
|
"max_count",
|
|
4354
|
-
"default"
|
|
4735
|
+
"default",
|
|
4736
|
+
"enum"
|
|
4355
4737
|
]);
|
|
4738
|
+
var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
|
|
4356
4739
|
var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
|
|
4357
4740
|
"fields",
|
|
4358
4741
|
"relationships",
|
|
@@ -4394,9 +4777,37 @@ function parseFieldOptions(raw, context, strict) {
|
|
|
4394
4777
|
if (raw.unique === true) opts.unique = true;
|
|
4395
4778
|
if (raw.required === true) opts.required = true;
|
|
4396
4779
|
if (raw.auto_assign === true) opts.autoAssign = true;
|
|
4780
|
+
if (raw.auto_stamp !== void 0) {
|
|
4781
|
+
if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
|
|
4782
|
+
throw new Error(
|
|
4783
|
+
`${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
|
|
4784
|
+
...VALID_AUTO_STAMP_VALUES
|
|
4785
|
+
].join(", ")}`
|
|
4786
|
+
);
|
|
4787
|
+
}
|
|
4788
|
+
opts.autoStamp = raw.auto_stamp;
|
|
4789
|
+
}
|
|
4397
4790
|
if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
|
|
4398
4791
|
if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
|
|
4399
4792
|
if (raw.default !== void 0) opts.default = raw.default;
|
|
4793
|
+
if (raw.enum !== void 0) {
|
|
4794
|
+
if (raw.type !== "string") {
|
|
4795
|
+
throw new Error(
|
|
4796
|
+
`${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
|
|
4797
|
+
);
|
|
4798
|
+
}
|
|
4799
|
+
if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
|
|
4800
|
+
throw new Error(
|
|
4801
|
+
`${context}: \`enum\` must be a non-empty array of strings.`
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4804
|
+
if (!raw.enum.every((v) => typeof v === "string")) {
|
|
4805
|
+
throw new Error(
|
|
4806
|
+
`${context}: \`enum\` values must all be strings.`
|
|
4807
|
+
);
|
|
4808
|
+
}
|
|
4809
|
+
opts.enum = [...raw.enum];
|
|
4810
|
+
}
|
|
4400
4811
|
return opts;
|
|
4401
4812
|
}
|
|
4402
4813
|
function requireField(raw, field, context) {
|
|
@@ -4520,3 +4931,4 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
4520
4931
|
|
|
4521
4932
|
// src/cloudflare-do.ts
|
|
4522
4933
|
init_metaSync();
|
|
4934
|
+
init_sql();
|