js-bao 0.5.1 → 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 +586 -265
- package/dist/browser.d.cts +21 -0
- package/dist/browser.d.ts +21 -0
- package/dist/browser.js +588 -263
- package/dist/client.cjs +42 -6
- package/dist/client.d.cts +71 -0
- package/dist/client.d.ts +71 -0
- package/dist/client.js +39 -4
- package/dist/cloudflare-do.cjs +462 -215
- package/dist/cloudflare-do.d.cts +111 -1
- package/dist/cloudflare-do.d.ts +111 -1
- package/dist/cloudflare-do.js +459 -213
- package/dist/cloudflare.cjs +270 -23
- package/dist/cloudflare.d.cts +276 -1
- package/dist/cloudflare.d.ts +276 -1
- package/dist/cloudflare.js +267 -21
- package/dist/codegen-v2.cjs +77 -19
- package/dist/codegen.cjs +2 -1
- package/dist/index.cjs +551 -224
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +549 -221
- package/dist/node.cjs +775 -291
- package/dist/node.d.cts +179 -1
- package/dist/node.d.ts +179 -1
- package/dist/node.js +762 -288
- package/package.json +2 -1
package/dist/cloudflare-do.cjs
CHANGED
|
@@ -256,21 +256,36 @@ var init_CursorManager = __esm({
|
|
|
256
256
|
return fields;
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
|
-
* 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.
|
|
260
272
|
*/
|
|
261
|
-
static buildOrderClause(sort, fieldFormatter = (field) => field) {
|
|
273
|
+
static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
|
|
262
274
|
const fields = this.extractSortFields(sort);
|
|
263
275
|
const clauses = [];
|
|
264
276
|
const directions = [];
|
|
277
|
+
const sqlDir = (declared) => {
|
|
278
|
+
const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
|
|
279
|
+
return effective === 1 ? "ASC" : "DESC";
|
|
280
|
+
};
|
|
265
281
|
if (sort && Object.keys(sort).length > 0) {
|
|
266
282
|
for (const [field, dir] of Object.entries(sort)) {
|
|
267
|
-
|
|
268
|
-
clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
|
|
283
|
+
clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
|
|
269
284
|
directions.push(dir);
|
|
270
285
|
}
|
|
271
286
|
}
|
|
272
287
|
if (!sort || !sort.hasOwnProperty("id")) {
|
|
273
|
-
clauses.push(`${fieldFormatter("id")}
|
|
288
|
+
clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
|
|
274
289
|
directions.push(1);
|
|
275
290
|
} else if (sort && sort.hasOwnProperty("id")) {
|
|
276
291
|
directions.push(sort["id"]);
|
|
@@ -293,18 +308,27 @@ var init_CursorManager = __esm({
|
|
|
293
308
|
/**
|
|
294
309
|
* Generate next and previous cursors from result set
|
|
295
310
|
*/
|
|
296
|
-
static generateResultCursors(results, sortFields,
|
|
311
|
+
static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
|
|
297
312
|
if (results.length === 0) {
|
|
298
313
|
return {};
|
|
299
314
|
}
|
|
300
315
|
const cursors = {};
|
|
301
316
|
if (hasMore && results.length > 0) {
|
|
302
317
|
const lastResult = results[results.length - 1];
|
|
303
|
-
cursors.nextCursor = this.generateCursor(
|
|
318
|
+
cursors.nextCursor = this.generateCursor(
|
|
319
|
+
lastResult,
|
|
320
|
+
sortFields,
|
|
321
|
+
requestedDirection
|
|
322
|
+
);
|
|
304
323
|
}
|
|
305
324
|
if (results.length > 0 && !isFirstPage) {
|
|
306
325
|
const firstResult = results[0];
|
|
307
|
-
|
|
326
|
+
const opposite = requestedDirection === 1 ? -1 : 1;
|
|
327
|
+
cursors.prevCursor = this.generateCursor(
|
|
328
|
+
firstResult,
|
|
329
|
+
sortFields,
|
|
330
|
+
opposite
|
|
331
|
+
);
|
|
308
332
|
}
|
|
309
333
|
return cursors;
|
|
310
334
|
}
|
|
@@ -395,6 +419,31 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
395
419
|
}
|
|
396
420
|
});
|
|
397
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
|
+
|
|
398
447
|
// src/models/metaSync.ts
|
|
399
448
|
function inferFieldType(value) {
|
|
400
449
|
if (isStringSetYMap(value)) return "stringset";
|
|
@@ -416,7 +465,10 @@ function encodeDefault(value) {
|
|
|
416
465
|
if (value === void 0 || value === null) return void 0;
|
|
417
466
|
if (typeof value === "function") {
|
|
418
467
|
const name = KNOWN_FUNCTION_DEFAULTS.get(value);
|
|
419
|
-
return name ? `$${name}` :
|
|
468
|
+
return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
|
|
469
|
+
}
|
|
470
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
471
|
+
return `$${value}`;
|
|
420
472
|
}
|
|
421
473
|
return value;
|
|
422
474
|
}
|
|
@@ -442,7 +494,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
442
494
|
if (compoundConstraints.length > 0) {
|
|
443
495
|
let constraints = meta.get("_constraints");
|
|
444
496
|
if (!constraints) {
|
|
445
|
-
constraints = new
|
|
497
|
+
constraints = new Y4.Map();
|
|
446
498
|
meta.set("_constraints", constraints);
|
|
447
499
|
}
|
|
448
500
|
for (const constraint of compoundConstraints) {
|
|
@@ -453,7 +505,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
453
505
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
454
506
|
let rels = meta.get("_relationships");
|
|
455
507
|
if (!rels) {
|
|
456
|
-
rels = new
|
|
508
|
+
rels = new Y4.Map();
|
|
457
509
|
meta.set("_relationships", rels);
|
|
458
510
|
}
|
|
459
511
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
@@ -465,7 +517,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
465
517
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
466
518
|
let fieldMeta = metaMap.get(fieldName);
|
|
467
519
|
if (!fieldMeta) {
|
|
468
|
-
fieldMeta = new
|
|
520
|
+
fieldMeta = new Y4.Map();
|
|
469
521
|
metaMap.set(fieldName, fieldMeta);
|
|
470
522
|
}
|
|
471
523
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -482,7 +534,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
482
534
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
483
535
|
let cMeta = constraintsMap.get(constraint.name);
|
|
484
536
|
if (!cMeta) {
|
|
485
|
-
cMeta = new
|
|
537
|
+
cMeta = new Y4.Map();
|
|
486
538
|
constraintsMap.set(constraint.name, cMeta);
|
|
487
539
|
}
|
|
488
540
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -492,7 +544,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
492
544
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
493
545
|
let relMeta = relsMap.get(relName);
|
|
494
546
|
if (!relMeta) {
|
|
495
|
-
relMeta = new
|
|
547
|
+
relMeta = new Y4.Map();
|
|
496
548
|
relsMap.set(relName, relMeta);
|
|
497
549
|
}
|
|
498
550
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -507,7 +559,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
507
559
|
if (fieldName.startsWith("_")) continue;
|
|
508
560
|
let fieldMeta = meta.get(fieldName);
|
|
509
561
|
if (!fieldMeta) {
|
|
510
|
-
fieldMeta = new
|
|
562
|
+
fieldMeta = new Y4.Map();
|
|
511
563
|
meta.set(fieldName, fieldMeta);
|
|
512
564
|
}
|
|
513
565
|
if (!fieldMeta.has("type")) {
|
|
@@ -523,34 +575,49 @@ function setIfChanged(map, key, value) {
|
|
|
523
575
|
map.set(key, value);
|
|
524
576
|
}
|
|
525
577
|
}
|
|
526
|
-
var
|
|
578
|
+
var Y4, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
|
|
527
579
|
var init_metaSync = __esm({
|
|
528
580
|
"src/models/metaSync.ts"() {
|
|
529
581
|
"use strict";
|
|
530
|
-
|
|
582
|
+
Y4 = __toESM(require("yjs"), 1);
|
|
531
583
|
init_yjsValues();
|
|
584
|
+
init_writeLifecycle();
|
|
532
585
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
533
586
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
534
587
|
}
|
|
535
588
|
});
|
|
536
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
|
+
|
|
537
602
|
// src/models/BaseModel.ts
|
|
538
603
|
function generateULID() {
|
|
539
|
-
return (0,
|
|
604
|
+
return (0, import_ulid3.ulid)();
|
|
540
605
|
}
|
|
541
|
-
var
|
|
606
|
+
var Y6, import_ulid3;
|
|
542
607
|
var init_BaseModel = __esm({
|
|
543
608
|
"src/models/BaseModel.ts"() {
|
|
544
609
|
"use strict";
|
|
545
|
-
|
|
546
|
-
|
|
610
|
+
Y6 = __toESM(require("yjs"), 1);
|
|
611
|
+
import_ulid3 = require("ulid");
|
|
547
612
|
init_StringSet();
|
|
548
613
|
init_documentTypes();
|
|
549
614
|
init_DocumentQueryTranslator();
|
|
550
615
|
init_CursorManager();
|
|
551
616
|
init_sql();
|
|
552
617
|
init_metaSync();
|
|
618
|
+
init_writeLifecycle();
|
|
553
619
|
init_yjsValues();
|
|
620
|
+
init_uniqueIndex();
|
|
554
621
|
registerFunctionDefault(generateULID, "generate_ulid");
|
|
555
622
|
}
|
|
556
623
|
});
|
|
@@ -578,6 +645,7 @@ __export(cloudflare_do_exports, {
|
|
|
578
645
|
DurableObjectEngine: () => DurableObjectEngine,
|
|
579
646
|
JsonQueryTranslator: () => JsonQueryTranslator,
|
|
580
647
|
JsonSchemaDDL: () => JsonSchemaDDL,
|
|
648
|
+
PROJECTED_JSON_ALIAS_PREFIX: () => PROJECTED_JSON_ALIAS_PREFIX,
|
|
581
649
|
buildSafeAlias: () => buildSafeAlias,
|
|
582
650
|
checkAutoStampsReservedFields: () => checkAutoStampsReservedFields,
|
|
583
651
|
checkReservedFieldName: () => checkReservedFieldName,
|
|
@@ -590,6 +658,8 @@ __export(cloudflare_do_exports, {
|
|
|
590
658
|
handleRequest: () => handleRequest,
|
|
591
659
|
inferFieldType: () => inferFieldType,
|
|
592
660
|
loadSchemaFromTomlString: () => loadSchemaFromTomlString,
|
|
661
|
+
parseRecordRow: () => parseRecordRow,
|
|
662
|
+
projectedJsonAlias: () => projectedJsonAlias,
|
|
593
663
|
registerFunctionDefault: () => registerFunctionDefault,
|
|
594
664
|
schemaToToml: () => schemaToToml,
|
|
595
665
|
syncInferredMeta: () => syncInferredMeta,
|
|
@@ -1605,6 +1675,45 @@ var DurableObjectEngine = class extends JsonSchemaEngine {
|
|
|
1605
1675
|
init_queryTypes();
|
|
1606
1676
|
init_CursorManager();
|
|
1607
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
|
|
1608
1717
|
init_patterns();
|
|
1609
1718
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
|
|
1610
1719
|
var JsonQueryTranslator = class {
|
|
@@ -1649,7 +1758,8 @@ var JsonQueryTranslator = class {
|
|
|
1649
1758
|
const whereClause = this.translateFilter(filter);
|
|
1650
1759
|
const orderClause = CursorManager.buildOrderClause(
|
|
1651
1760
|
options?.sort,
|
|
1652
|
-
(field) => this.getFieldSql(field)
|
|
1761
|
+
(field) => this.getFieldSql(field),
|
|
1762
|
+
options?.direction || 1
|
|
1653
1763
|
);
|
|
1654
1764
|
const limitClause = this.buildLimitClause(options);
|
|
1655
1765
|
const paginationClause = this.buildPaginationClause(
|
|
@@ -2100,7 +2210,7 @@ var JsonQueryTranslator = class {
|
|
|
2100
2210
|
for (const field of includeFields) {
|
|
2101
2211
|
if (field === "id" || field === "type") continue;
|
|
2102
2212
|
selectParts.push(
|
|
2103
|
-
`
|
|
2213
|
+
`_data -> '$.${field}' AS ${quoteIdentifier(projectedJsonAlias(field))}`
|
|
2104
2214
|
);
|
|
2105
2215
|
}
|
|
2106
2216
|
return selectParts.join(", ");
|
|
@@ -2287,6 +2397,13 @@ var JsonQueryTranslator = class {
|
|
|
2287
2397
|
init_CursorManager();
|
|
2288
2398
|
init_sql();
|
|
2289
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
|
+
};
|
|
2290
2407
|
function checkReservedFieldName(fieldName) {
|
|
2291
2408
|
if (fieldName === "id") return null;
|
|
2292
2409
|
if (fieldName === "type") {
|
|
@@ -2331,6 +2448,35 @@ function applyAutoStamps(data, autoStamps, isNew) {
|
|
|
2331
2448
|
}
|
|
2332
2449
|
}
|
|
2333
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
|
+
}
|
|
2334
2480
|
function createDatabaseDO(config = {}) {
|
|
2335
2481
|
const hooks = config.hooks;
|
|
2336
2482
|
return class DocumentDO {
|
|
@@ -2488,6 +2634,16 @@ function createDatabaseDO(config = {}) {
|
|
|
2488
2634
|
hasMore,
|
|
2489
2635
|
isFirstPage
|
|
2490
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
|
+
}
|
|
2491
2647
|
const response = {
|
|
2492
2648
|
data,
|
|
2493
2649
|
hasMore,
|
|
@@ -2590,6 +2746,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2590
2746
|
}
|
|
2591
2747
|
const isNew = !this._engine.recordExists(modelName, id);
|
|
2592
2748
|
applyAutoStamps(data, autoStamps, isNew);
|
|
2749
|
+
let hookAppliedNames;
|
|
2593
2750
|
if (hooks?.beforeSave) {
|
|
2594
2751
|
const ctx = {
|
|
2595
2752
|
modelName,
|
|
@@ -2605,6 +2762,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2605
2762
|
if (!result.allow) {
|
|
2606
2763
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2607
2764
|
}
|
|
2765
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2608
2766
|
}
|
|
2609
2767
|
const existingForUnique = this._engine.execSqlSync(
|
|
2610
2768
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2634,6 +2792,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2634
2792
|
}
|
|
2635
2793
|
this._engine.trackModelFields(modelName, data);
|
|
2636
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;
|
|
2637
2800
|
return Response.json(response);
|
|
2638
2801
|
}
|
|
2639
2802
|
/** @internal — Partial update: merge provided fields into existing record */
|
|
@@ -2663,6 +2826,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2663
2826
|
);
|
|
2664
2827
|
}
|
|
2665
2828
|
applyAutoStamps(data, autoStamps, false);
|
|
2829
|
+
let hookAppliedNames;
|
|
2666
2830
|
if (hooks?.beforeSave) {
|
|
2667
2831
|
const ctx = {
|
|
2668
2832
|
modelName,
|
|
@@ -2678,6 +2842,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2678
2842
|
if (!result.allow) {
|
|
2679
2843
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2680
2844
|
}
|
|
2845
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2681
2846
|
}
|
|
2682
2847
|
const existing = this._engine.execSqlSync(
|
|
2683
2848
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2701,6 +2866,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2701
2866
|
}
|
|
2702
2867
|
this._engine.trackModelFields(modelName, data);
|
|
2703
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;
|
|
2704
2874
|
return Response.json(response);
|
|
2705
2875
|
}
|
|
2706
2876
|
/** @internal */
|
|
@@ -2742,7 +2912,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2742
2912
|
/** @internal — Execute multiple save/patch/delete operations in a single transaction */
|
|
2743
2913
|
async _handleBatch(request, docId) {
|
|
2744
2914
|
const body = await request.json();
|
|
2745
|
-
const { operations } = body;
|
|
2915
|
+
const { operations, atomic = false } = body;
|
|
2746
2916
|
if (!operations || !Array.isArray(operations) || operations.length === 0) {
|
|
2747
2917
|
return this._errorResponse("operations array is required and must not be empty", 400);
|
|
2748
2918
|
}
|
|
@@ -2750,6 +2920,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2750
2920
|
const resolvedBatchIds = /* @__PURE__ */ new Map();
|
|
2751
2921
|
const opErrors = /* @__PURE__ */ new Map();
|
|
2752
2922
|
const batchCreatedIds = /* @__PURE__ */ new Set();
|
|
2923
|
+
const batchIsNew = /* @__PURE__ */ new Map();
|
|
2924
|
+
const batchAppliedNames = /* @__PURE__ */ new Map();
|
|
2753
2925
|
for (let i = 0; i < operations.length; i++) {
|
|
2754
2926
|
const op = operations[i];
|
|
2755
2927
|
if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
|
|
@@ -2823,6 +2995,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2823
2995
|
if (op.op === "save" && isNew && effectiveId) {
|
|
2824
2996
|
batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
|
|
2825
2997
|
}
|
|
2998
|
+
batchIsNew.set(i, isNew);
|
|
2826
2999
|
applyAutoStamps(op.data, op.autoStamps, isNew);
|
|
2827
3000
|
if (hooks?.beforeSave) {
|
|
2828
3001
|
const ctx = {
|
|
@@ -2838,6 +3011,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2838
3011
|
const result = await hooks.beforeSave(ctx);
|
|
2839
3012
|
if (!result.allow) {
|
|
2840
3013
|
hookDenials.set(i, result.reason || "Save denied");
|
|
3014
|
+
} else if (result.serverAppliedFieldNames) {
|
|
3015
|
+
batchAppliedNames.set(i, result.serverAppliedFieldNames);
|
|
2841
3016
|
}
|
|
2842
3017
|
}
|
|
2843
3018
|
} else if (op.op === "delete") {
|
|
@@ -2886,205 +3061,263 @@ function createDatabaseDO(config = {}) {
|
|
|
2886
3061
|
}
|
|
2887
3062
|
const results = [];
|
|
2888
3063
|
const fieldsToTrack = [];
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
2911
|
-
results.push({
|
|
2912
|
-
success: false,
|
|
2913
|
-
id: conditionId,
|
|
2914
|
-
error: `Condition not met for ${op.modelName}/${conditionId}`
|
|
2915
|
-
});
|
|
2916
|
-
continue;
|
|
2917
|
-
}
|
|
2918
|
-
if (op.op === "save") {
|
|
2919
|
-
let opId = resolvedBatchIds.get(i) || op.id;
|
|
2920
|
-
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
2921
|
-
results.push({
|
|
2922
|
-
success: false,
|
|
2923
|
-
id: opId,
|
|
2924
|
-
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"
|
|
2925
3085
|
});
|
|
2926
3086
|
continue;
|
|
2927
3087
|
}
|
|
2928
|
-
if (
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
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, {
|
|
2946
3111
|
id: opId,
|
|
2947
|
-
error:
|
|
3112
|
+
error: `Record ${op.modelName}/${opId} already exists`,
|
|
3113
|
+
status: 409,
|
|
3114
|
+
code: "ALREADY_EXISTS"
|
|
2948
3115
|
});
|
|
2949
3116
|
continue;
|
|
2950
3117
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
const dataJson = JSON.stringify(jsonFields);
|
|
2956
|
-
this._engine.execSqlSync(
|
|
2957
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2958
|
-
[opId, op.modelName, dataJson]
|
|
2959
|
-
);
|
|
2960
|
-
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
2961
|
-
this._engine.execSqlSync(
|
|
2962
|
-
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
2963
|
-
[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]
|
|
2964
3122
|
);
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
);
|
|
3123
|
+
let dataForUnique = op.data;
|
|
3124
|
+
if (existingForUnique.length > 0) {
|
|
3125
|
+
const existingData = JSON.parse(existingForUnique[0]._data);
|
|
3126
|
+
dataForUnique = { ...existingData, ...op.data };
|
|
2970
3127
|
}
|
|
2971
|
-
}
|
|
2972
|
-
} else {
|
|
2973
|
-
const { id: _id, ...fieldsForJson } = op.data;
|
|
2974
|
-
const dataJson = JSON.stringify(fieldsForJson);
|
|
2975
|
-
this._engine.execSqlSync(
|
|
2976
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2977
|
-
[opId, op.modelName, dataJson]
|
|
2978
|
-
);
|
|
2979
|
-
}
|
|
2980
|
-
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
2981
|
-
results.push({ success: true, id: opId });
|
|
2982
|
-
} else if (op.op === "patch") {
|
|
2983
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
2984
|
-
results.push({
|
|
2985
|
-
success: false,
|
|
2986
|
-
id: op.id,
|
|
2987
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2988
|
-
});
|
|
2989
|
-
continue;
|
|
2990
|
-
}
|
|
2991
|
-
if (op.data) {
|
|
2992
|
-
const existing = this._engine.execSqlSync(
|
|
2993
|
-
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
2994
|
-
[op.id, op.modelName]
|
|
2995
|
-
);
|
|
2996
|
-
if (existing.length > 0) {
|
|
2997
|
-
const existingData = JSON.parse(existing[0]._data);
|
|
2998
|
-
const mergedData = { ...existingData, ...op.data };
|
|
2999
3128
|
const violation = this._engine.checkUniqueConstraints(
|
|
3000
3129
|
op.modelName,
|
|
3001
|
-
|
|
3002
|
-
|
|
3130
|
+
opId,
|
|
3131
|
+
dataForUnique
|
|
3003
3132
|
);
|
|
3004
3133
|
if (violation) {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3134
|
+
pushFailure(i, {
|
|
3135
|
+
id: opId,
|
|
3136
|
+
error: violation,
|
|
3137
|
+
status: 409,
|
|
3138
|
+
code: "UNIQUE_VIOLATION"
|
|
3009
3139
|
});
|
|
3010
3140
|
continue;
|
|
3011
3141
|
}
|
|
3012
3142
|
}
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
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
|
+
}
|
|
3027
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
|
+
);
|
|
3028
3300
|
results.push({ success: true, id: op.id });
|
|
3029
3301
|
}
|
|
3030
|
-
} else if (op.op === "delete") {
|
|
3031
|
-
this._engine.execSqlSync(
|
|
3032
|
-
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
3033
|
-
[op.id, op.modelName]
|
|
3034
|
-
);
|
|
3035
|
-
this._engine.execSqlSync(
|
|
3036
|
-
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
3037
|
-
[op.id, op.modelName]
|
|
3038
|
-
);
|
|
3039
|
-
results.push({ success: true, id: op.id });
|
|
3040
|
-
} else if (op.op === "increment") {
|
|
3041
|
-
const newValues = this._engine.incrementFieldsRaw(
|
|
3042
|
-
op.modelName,
|
|
3043
|
-
op.id,
|
|
3044
|
-
op.fields
|
|
3045
|
-
);
|
|
3046
|
-
if (!newValues) {
|
|
3047
|
-
results.push({
|
|
3048
|
-
success: false,
|
|
3049
|
-
id: op.id,
|
|
3050
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3051
|
-
});
|
|
3052
|
-
} else {
|
|
3053
|
-
results.push({ success: true, id: op.id, values: newValues });
|
|
3054
|
-
}
|
|
3055
|
-
} else if (op.op === "addToSet") {
|
|
3056
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3057
|
-
results.push({
|
|
3058
|
-
success: false,
|
|
3059
|
-
id: op.id,
|
|
3060
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3061
|
-
});
|
|
3062
|
-
continue;
|
|
3063
|
-
}
|
|
3064
|
-
this._engine.addToStringSetsRaw(
|
|
3065
|
-
op.modelName,
|
|
3066
|
-
op.id,
|
|
3067
|
-
op.stringSets
|
|
3068
|
-
);
|
|
3069
|
-
results.push({ success: true, id: op.id });
|
|
3070
|
-
} else if (op.op === "removeFromSet") {
|
|
3071
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3072
|
-
results.push({
|
|
3073
|
-
success: false,
|
|
3074
|
-
id: op.id,
|
|
3075
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3076
|
-
});
|
|
3077
|
-
continue;
|
|
3078
|
-
}
|
|
3079
|
-
this._engine.removeFromStringSetsRaw(
|
|
3080
|
-
op.modelName,
|
|
3081
|
-
op.id,
|
|
3082
|
-
op.stringSets
|
|
3083
|
-
);
|
|
3084
|
-
results.push({ success: true, id: op.id });
|
|
3085
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 });
|
|
3086
3318
|
}
|
|
3087
|
-
|
|
3319
|
+
throw err;
|
|
3320
|
+
}
|
|
3088
3321
|
for (const entry of fieldsToTrack) {
|
|
3089
3322
|
this._engine.trackModelFields(entry.modelName, entry.data);
|
|
3090
3323
|
}
|
|
@@ -3454,6 +3687,18 @@ function createDatabaseDO(config = {}) {
|
|
|
3454
3687
|
}
|
|
3455
3688
|
return facetResult;
|
|
3456
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
|
+
}
|
|
3457
3702
|
const nestedResult = {};
|
|
3458
3703
|
const aliasLookup = /* @__PURE__ */ new Map();
|
|
3459
3704
|
for (const entry of aliasMap) {
|
|
@@ -3781,16 +4026,7 @@ function createDatabaseDO(config = {}) {
|
|
|
3781
4026
|
}
|
|
3782
4027
|
/** @internal */
|
|
3783
4028
|
_parseRow(row) {
|
|
3784
|
-
|
|
3785
|
-
let parsed = {};
|
|
3786
|
-
if (_data) {
|
|
3787
|
-
try {
|
|
3788
|
-
parsed = JSON.parse(_data);
|
|
3789
|
-
} catch (e) {
|
|
3790
|
-
console.warn("Failed to parse _data:", e);
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
return { id: _id, type: _type, ...parsed, ...rest };
|
|
4029
|
+
return parseRecordRow(row);
|
|
3794
4030
|
}
|
|
3795
4031
|
// ─── Include (Related Data Loading) ──────────────────────────────
|
|
3796
4032
|
/** @internal — Validate an IncludeSpec, returning an error message or null */
|
|
@@ -3920,8 +4156,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3920
4156
|
spec.filter,
|
|
3921
4157
|
{ projection: spec.projection }
|
|
3922
4158
|
);
|
|
4159
|
+
const relatedProjected = spec.projection ? related.map((r) => this._applyProjection(r, spec.projection)) : related;
|
|
3923
4160
|
const lookupMap = /* @__PURE__ */ new Map();
|
|
3924
|
-
for (const r of
|
|
4161
|
+
for (const r of relatedProjected) {
|
|
3925
4162
|
lookupMap.set(r.id, r);
|
|
3926
4163
|
}
|
|
3927
4164
|
for (const rec of records) {
|
|
@@ -3954,8 +4191,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3954
4191
|
spec.filter,
|
|
3955
4192
|
{ projection: spec.projection }
|
|
3956
4193
|
);
|
|
4194
|
+
const targetsProjected = spec.projection ? targets.map((t) => this._applyProjection(t, spec.projection)) : targets;
|
|
3957
4195
|
const targetLookup = /* @__PURE__ */ new Map();
|
|
3958
|
-
for (const t of
|
|
4196
|
+
for (const t of targetsProjected) {
|
|
3959
4197
|
targetLookup.set(t.id, t);
|
|
3960
4198
|
}
|
|
3961
4199
|
for (const rec of records) {
|
|
@@ -4099,6 +4337,14 @@ function createDatabaseDO(config = {}) {
|
|
|
4099
4337
|
return allResults;
|
|
4100
4338
|
}
|
|
4101
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.
|
|
4102
4348
|
_applyProjection(record, projection) {
|
|
4103
4349
|
const fields = Object.entries(projection);
|
|
4104
4350
|
if (fields.length === 0) return record;
|
|
@@ -4117,7 +4363,7 @@ function createDatabaseDO(config = {}) {
|
|
|
4117
4363
|
} else {
|
|
4118
4364
|
const result = { ...record };
|
|
4119
4365
|
for (const [f, v] of fields) {
|
|
4120
|
-
if (v === 0) delete result[f];
|
|
4366
|
+
if (v === 0 && f !== "id" && f !== "type") delete result[f];
|
|
4121
4367
|
}
|
|
4122
4368
|
return result;
|
|
4123
4369
|
}
|
|
@@ -4395,6 +4641,7 @@ var import_smol_toml = require("smol-toml");
|
|
|
4395
4641
|
// src/models/schema.ts
|
|
4396
4642
|
init_BaseModel();
|
|
4397
4643
|
init_ModelRegistry();
|
|
4644
|
+
init_uniqueIndex();
|
|
4398
4645
|
function defineModelSchema(input) {
|
|
4399
4646
|
const { name, fields } = input;
|
|
4400
4647
|
const options = {
|
|
@@ -4437,7 +4684,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
|
4437
4684
|
for (const [fieldName, options] of fields.entries()) {
|
|
4438
4685
|
if (options.unique) {
|
|
4439
4686
|
resolved.push({
|
|
4440
|
-
name:
|
|
4687
|
+
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
4441
4688
|
fields: [fieldName]
|
|
4442
4689
|
});
|
|
4443
4690
|
}
|