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.js
CHANGED
|
@@ -229,21 +229,36 @@ var init_CursorManager = __esm({
|
|
|
229
229
|
return fields;
|
|
230
230
|
}
|
|
231
231
|
/**
|
|
232
|
-
* Build ORDER BY clause from sort specification
|
|
232
|
+
* Build ORDER BY clause from sort specification.
|
|
233
|
+
*
|
|
234
|
+
* `direction` is the pagination direction (1 = forward, -1 = backward).
|
|
235
|
+
* For backward pagination each field's effective ORDER BY direction is
|
|
236
|
+
* reversed so the cursor's flipped inequality (built by
|
|
237
|
+
* `buildPaginationConditions`) returns the page adjacent to the cursor,
|
|
238
|
+
* not the globally smallest/largest window. Without this,
|
|
239
|
+
* `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
|
|
240
|
+
* rows overall instead of the n rows just below the cursor.
|
|
241
|
+
*
|
|
242
|
+
* The returned `directions` array carries the *declared* per-field sort
|
|
243
|
+
* directions (unaffected by `direction`) — `buildPaginationConditions`
|
|
244
|
+
* needs the declared directions to pick each field's comparison operator.
|
|
233
245
|
*/
|
|
234
|
-
static buildOrderClause(sort, fieldFormatter = (field) => field) {
|
|
246
|
+
static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
|
|
235
247
|
const fields = this.extractSortFields(sort);
|
|
236
248
|
const clauses = [];
|
|
237
249
|
const directions = [];
|
|
250
|
+
const sqlDir = (declared) => {
|
|
251
|
+
const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
|
|
252
|
+
return effective === 1 ? "ASC" : "DESC";
|
|
253
|
+
};
|
|
238
254
|
if (sort && Object.keys(sort).length > 0) {
|
|
239
255
|
for (const [field, dir] of Object.entries(sort)) {
|
|
240
|
-
|
|
241
|
-
clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
|
|
256
|
+
clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
|
|
242
257
|
directions.push(dir);
|
|
243
258
|
}
|
|
244
259
|
}
|
|
245
260
|
if (!sort || !sort.hasOwnProperty("id")) {
|
|
246
|
-
clauses.push(`${fieldFormatter("id")}
|
|
261
|
+
clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
|
|
247
262
|
directions.push(1);
|
|
248
263
|
} else if (sort && sort.hasOwnProperty("id")) {
|
|
249
264
|
directions.push(sort["id"]);
|
|
@@ -266,18 +281,27 @@ var init_CursorManager = __esm({
|
|
|
266
281
|
/**
|
|
267
282
|
* Generate next and previous cursors from result set
|
|
268
283
|
*/
|
|
269
|
-
static generateResultCursors(results, sortFields,
|
|
284
|
+
static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
|
|
270
285
|
if (results.length === 0) {
|
|
271
286
|
return {};
|
|
272
287
|
}
|
|
273
288
|
const cursors = {};
|
|
274
289
|
if (hasMore && results.length > 0) {
|
|
275
290
|
const lastResult = results[results.length - 1];
|
|
276
|
-
cursors.nextCursor = this.generateCursor(
|
|
291
|
+
cursors.nextCursor = this.generateCursor(
|
|
292
|
+
lastResult,
|
|
293
|
+
sortFields,
|
|
294
|
+
requestedDirection
|
|
295
|
+
);
|
|
277
296
|
}
|
|
278
297
|
if (results.length > 0 && !isFirstPage) {
|
|
279
298
|
const firstResult = results[0];
|
|
280
|
-
|
|
299
|
+
const opposite = requestedDirection === 1 ? -1 : 1;
|
|
300
|
+
cursors.prevCursor = this.generateCursor(
|
|
301
|
+
firstResult,
|
|
302
|
+
sortFields,
|
|
303
|
+
opposite
|
|
304
|
+
);
|
|
281
305
|
}
|
|
282
306
|
return cursors;
|
|
283
307
|
}
|
|
@@ -367,8 +391,33 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
367
391
|
}
|
|
368
392
|
});
|
|
369
393
|
|
|
370
|
-
// src/models/
|
|
394
|
+
// src/models/writeLifecycle.ts
|
|
371
395
|
import * as Y3 from "yjs";
|
|
396
|
+
import { ulid as ulid2 } from "ulid";
|
|
397
|
+
function registerFunctionDefaultGenerator(name, fn) {
|
|
398
|
+
FUNCTION_DEFAULT_GENERATORS.set(name, fn);
|
|
399
|
+
}
|
|
400
|
+
var KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
|
|
401
|
+
var init_writeLifecycle = __esm({
|
|
402
|
+
"src/models/writeLifecycle.ts"() {
|
|
403
|
+
"use strict";
|
|
404
|
+
KNOWN_FIELD_TYPE_MAP = {
|
|
405
|
+
string: true,
|
|
406
|
+
number: true,
|
|
407
|
+
boolean: true,
|
|
408
|
+
date: true,
|
|
409
|
+
id: true,
|
|
410
|
+
stringset: true
|
|
411
|
+
};
|
|
412
|
+
KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
|
|
413
|
+
FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
|
|
414
|
+
registerFunctionDefaultGenerator("generate_ulid", () => ulid2());
|
|
415
|
+
UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// src/models/metaSync.ts
|
|
420
|
+
import * as Y4 from "yjs";
|
|
372
421
|
function inferFieldType(value) {
|
|
373
422
|
if (isStringSetYMap(value)) return "stringset";
|
|
374
423
|
switch (typeof value) {
|
|
@@ -389,7 +438,10 @@ function encodeDefault(value) {
|
|
|
389
438
|
if (value === void 0 || value === null) return void 0;
|
|
390
439
|
if (typeof value === "function") {
|
|
391
440
|
const name = KNOWN_FUNCTION_DEFAULTS.get(value);
|
|
392
|
-
return name ? `$${name}` :
|
|
441
|
+
return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
|
|
442
|
+
}
|
|
443
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
444
|
+
return `$${value}`;
|
|
393
445
|
}
|
|
394
446
|
return value;
|
|
395
447
|
}
|
|
@@ -415,7 +467,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
415
467
|
if (compoundConstraints.length > 0) {
|
|
416
468
|
let constraints = meta.get("_constraints");
|
|
417
469
|
if (!constraints) {
|
|
418
|
-
constraints = new
|
|
470
|
+
constraints = new Y4.Map();
|
|
419
471
|
meta.set("_constraints", constraints);
|
|
420
472
|
}
|
|
421
473
|
for (const constraint of compoundConstraints) {
|
|
@@ -426,7 +478,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
426
478
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
427
479
|
let rels = meta.get("_relationships");
|
|
428
480
|
if (!rels) {
|
|
429
|
-
rels = new
|
|
481
|
+
rels = new Y4.Map();
|
|
430
482
|
meta.set("_relationships", rels);
|
|
431
483
|
}
|
|
432
484
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
@@ -438,7 +490,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
438
490
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
439
491
|
let fieldMeta = metaMap.get(fieldName);
|
|
440
492
|
if (!fieldMeta) {
|
|
441
|
-
fieldMeta = new
|
|
493
|
+
fieldMeta = new Y4.Map();
|
|
442
494
|
metaMap.set(fieldName, fieldMeta);
|
|
443
495
|
}
|
|
444
496
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -455,7 +507,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
455
507
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
456
508
|
let cMeta = constraintsMap.get(constraint.name);
|
|
457
509
|
if (!cMeta) {
|
|
458
|
-
cMeta = new
|
|
510
|
+
cMeta = new Y4.Map();
|
|
459
511
|
constraintsMap.set(constraint.name, cMeta);
|
|
460
512
|
}
|
|
461
513
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -465,7 +517,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
465
517
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
466
518
|
let relMeta = relsMap.get(relName);
|
|
467
519
|
if (!relMeta) {
|
|
468
|
-
relMeta = new
|
|
520
|
+
relMeta = new Y4.Map();
|
|
469
521
|
relsMap.set(relName, relMeta);
|
|
470
522
|
}
|
|
471
523
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -480,7 +532,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
480
532
|
if (fieldName.startsWith("_")) continue;
|
|
481
533
|
let fieldMeta = meta.get(fieldName);
|
|
482
534
|
if (!fieldMeta) {
|
|
483
|
-
fieldMeta = new
|
|
535
|
+
fieldMeta = new Y4.Map();
|
|
484
536
|
meta.set(fieldName, fieldMeta);
|
|
485
537
|
}
|
|
486
538
|
if (!fieldMeta.has("type")) {
|
|
@@ -501,16 +553,28 @@ var init_metaSync = __esm({
|
|
|
501
553
|
"src/models/metaSync.ts"() {
|
|
502
554
|
"use strict";
|
|
503
555
|
init_yjsValues();
|
|
556
|
+
init_writeLifecycle();
|
|
504
557
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
505
558
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
506
559
|
}
|
|
507
560
|
});
|
|
508
561
|
|
|
562
|
+
// src/utils/uniqueIndex.ts
|
|
563
|
+
import * as Y5 from "yjs";
|
|
564
|
+
function fieldUniqueConstraintName(modelName, fieldName) {
|
|
565
|
+
return `${modelName}_${fieldName}_unique`;
|
|
566
|
+
}
|
|
567
|
+
var init_uniqueIndex = __esm({
|
|
568
|
+
"src/utils/uniqueIndex.ts"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
509
573
|
// src/models/BaseModel.ts
|
|
510
|
-
import * as
|
|
511
|
-
import { ulid as
|
|
574
|
+
import * as Y6 from "yjs";
|
|
575
|
+
import { ulid as ulid3 } from "ulid";
|
|
512
576
|
function generateULID() {
|
|
513
|
-
return
|
|
577
|
+
return ulid3();
|
|
514
578
|
}
|
|
515
579
|
var init_BaseModel = __esm({
|
|
516
580
|
"src/models/BaseModel.ts"() {
|
|
@@ -521,7 +585,9 @@ var init_BaseModel = __esm({
|
|
|
521
585
|
init_CursorManager();
|
|
522
586
|
init_sql();
|
|
523
587
|
init_metaSync();
|
|
588
|
+
init_writeLifecycle();
|
|
524
589
|
init_yjsValues();
|
|
590
|
+
init_uniqueIndex();
|
|
525
591
|
registerFunctionDefault(generateULID, "generate_ulid");
|
|
526
592
|
}
|
|
527
593
|
});
|
|
@@ -1551,6 +1617,45 @@ var DurableObjectEngine = class extends JsonSchemaEngine {
|
|
|
1551
1617
|
init_queryTypes();
|
|
1552
1618
|
init_CursorManager();
|
|
1553
1619
|
init_sql();
|
|
1620
|
+
|
|
1621
|
+
// src/query/recordRow.ts
|
|
1622
|
+
var PROJECTED_JSON_ALIAS_PREFIX = "_json_";
|
|
1623
|
+
function projectedJsonAlias(fieldName) {
|
|
1624
|
+
return `${PROJECTED_JSON_ALIAS_PREFIX}${fieldName}`;
|
|
1625
|
+
}
|
|
1626
|
+
function parseRecordRow(row) {
|
|
1627
|
+
const { _id, _type, _data, ...rest } = row;
|
|
1628
|
+
let parsed = {};
|
|
1629
|
+
if (_data) {
|
|
1630
|
+
try {
|
|
1631
|
+
parsed = JSON.parse(_data);
|
|
1632
|
+
} catch (e) {
|
|
1633
|
+
console.warn("Failed to parse _data:", e);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return { id: _id, type: _type, ...parsed, ...decodeProjectedColumns(rest) };
|
|
1637
|
+
}
|
|
1638
|
+
function decodeProjectedColumns(columns) {
|
|
1639
|
+
const decoded = {};
|
|
1640
|
+
for (const [column, value] of Object.entries(columns)) {
|
|
1641
|
+
if (!column.startsWith(PROJECTED_JSON_ALIAS_PREFIX)) {
|
|
1642
|
+
decoded[column] = value;
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
decoded[column.slice(PROJECTED_JSON_ALIAS_PREFIX.length)] = parseJsonColumn(value);
|
|
1646
|
+
}
|
|
1647
|
+
return decoded;
|
|
1648
|
+
}
|
|
1649
|
+
function parseJsonColumn(value) {
|
|
1650
|
+
if (typeof value !== "string") return value;
|
|
1651
|
+
try {
|
|
1652
|
+
return JSON.parse(value);
|
|
1653
|
+
} catch {
|
|
1654
|
+
return value;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// src/query/JsonQueryTranslator.ts
|
|
1554
1659
|
init_patterns();
|
|
1555
1660
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
|
|
1556
1661
|
var JsonQueryTranslator = class {
|
|
@@ -1595,7 +1700,8 @@ var JsonQueryTranslator = class {
|
|
|
1595
1700
|
const whereClause = this.translateFilter(filter);
|
|
1596
1701
|
const orderClause = CursorManager.buildOrderClause(
|
|
1597
1702
|
options?.sort,
|
|
1598
|
-
(field) => this.getFieldSql(field)
|
|
1703
|
+
(field) => this.getFieldSql(field),
|
|
1704
|
+
options?.direction || 1
|
|
1599
1705
|
);
|
|
1600
1706
|
const limitClause = this.buildLimitClause(options);
|
|
1601
1707
|
const paginationClause = this.buildPaginationClause(
|
|
@@ -2046,7 +2152,7 @@ var JsonQueryTranslator = class {
|
|
|
2046
2152
|
for (const field of includeFields) {
|
|
2047
2153
|
if (field === "id" || field === "type") continue;
|
|
2048
2154
|
selectParts.push(
|
|
2049
|
-
`
|
|
2155
|
+
`_data -> '$.${field}' AS ${quoteIdentifier(projectedJsonAlias(field))}`
|
|
2050
2156
|
);
|
|
2051
2157
|
}
|
|
2052
2158
|
return selectParts.join(", ");
|
|
@@ -2233,6 +2339,13 @@ var JsonQueryTranslator = class {
|
|
|
2233
2339
|
init_CursorManager();
|
|
2234
2340
|
init_sql();
|
|
2235
2341
|
import { ulid } from "ulid";
|
|
2342
|
+
var AtomicBatchAbort = class extends Error {
|
|
2343
|
+
constructor(failedIndex) {
|
|
2344
|
+
super(`atomic batch aborted at operation ${failedIndex}`);
|
|
2345
|
+
this.failedIndex = failedIndex;
|
|
2346
|
+
this.name = "AtomicBatchAbort";
|
|
2347
|
+
}
|
|
2348
|
+
};
|
|
2236
2349
|
function checkReservedFieldName(fieldName) {
|
|
2237
2350
|
if (fieldName === "id") return null;
|
|
2238
2351
|
if (fieldName === "type") {
|
|
@@ -2277,6 +2390,35 @@ function applyAutoStamps(data, autoStamps, isNew) {
|
|
|
2277
2390
|
}
|
|
2278
2391
|
}
|
|
2279
2392
|
}
|
|
2393
|
+
function collectAppliedStamps(data, autoStamps, isNew) {
|
|
2394
|
+
if (!autoStamps || !data) return void 0;
|
|
2395
|
+
const stamps = {};
|
|
2396
|
+
const take = (fields) => {
|
|
2397
|
+
if (!fields) return;
|
|
2398
|
+
for (const field of fields) {
|
|
2399
|
+
const value = data[field];
|
|
2400
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2401
|
+
stamps[field] = value;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
if (isNew) take(autoStamps.create);
|
|
2406
|
+
take(autoStamps.update);
|
|
2407
|
+
return Object.keys(stamps).length > 0 ? stamps : void 0;
|
|
2408
|
+
}
|
|
2409
|
+
function collectAppliedValues(data, fieldNames) {
|
|
2410
|
+
if (!data || !fieldNames || fieldNames.length === 0) return void 0;
|
|
2411
|
+
const out = {};
|
|
2412
|
+
for (const name of fieldNames) {
|
|
2413
|
+
const value = data[name];
|
|
2414
|
+
if (value !== void 0) out[name] = value;
|
|
2415
|
+
}
|
|
2416
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2417
|
+
}
|
|
2418
|
+
function mergeAppliedFields(stamps, applied) {
|
|
2419
|
+
if (!stamps && !applied) return void 0;
|
|
2420
|
+
return { ...stamps ?? {}, ...applied ?? {} };
|
|
2421
|
+
}
|
|
2280
2422
|
function createDatabaseDO(config = {}) {
|
|
2281
2423
|
const hooks = config.hooks;
|
|
2282
2424
|
return class DocumentDO {
|
|
@@ -2434,6 +2576,16 @@ function createDatabaseDO(config = {}) {
|
|
|
2434
2576
|
hasMore,
|
|
2435
2577
|
isFirstPage
|
|
2436
2578
|
);
|
|
2579
|
+
if (options?.projection && Object.keys(options.projection).length > 0) {
|
|
2580
|
+
const isExclusion = Object.values(options.projection).some(
|
|
2581
|
+
(v) => v === 0
|
|
2582
|
+
);
|
|
2583
|
+
if (isExclusion) {
|
|
2584
|
+
data = data.map(
|
|
2585
|
+
(rec) => this._applyProjection(rec, options.projection)
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2437
2589
|
const response = {
|
|
2438
2590
|
data,
|
|
2439
2591
|
hasMore,
|
|
@@ -2536,6 +2688,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2536
2688
|
}
|
|
2537
2689
|
const isNew = !this._engine.recordExists(modelName, id);
|
|
2538
2690
|
applyAutoStamps(data, autoStamps, isNew);
|
|
2691
|
+
let hookAppliedNames;
|
|
2539
2692
|
if (hooks?.beforeSave) {
|
|
2540
2693
|
const ctx = {
|
|
2541
2694
|
modelName,
|
|
@@ -2551,6 +2704,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2551
2704
|
if (!result.allow) {
|
|
2552
2705
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2553
2706
|
}
|
|
2707
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2554
2708
|
}
|
|
2555
2709
|
const existingForUnique = this._engine.execSqlSync(
|
|
2556
2710
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2580,6 +2734,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2580
2734
|
}
|
|
2581
2735
|
this._engine.trackModelFields(modelName, data);
|
|
2582
2736
|
const response = { success: true, id };
|
|
2737
|
+
const appliedFields = mergeAppliedFields(
|
|
2738
|
+
collectAppliedStamps(data, autoStamps, isNew),
|
|
2739
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
2740
|
+
);
|
|
2741
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2583
2742
|
return Response.json(response);
|
|
2584
2743
|
}
|
|
2585
2744
|
/** @internal — Partial update: merge provided fields into existing record */
|
|
@@ -2609,6 +2768,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2609
2768
|
);
|
|
2610
2769
|
}
|
|
2611
2770
|
applyAutoStamps(data, autoStamps, false);
|
|
2771
|
+
let hookAppliedNames;
|
|
2612
2772
|
if (hooks?.beforeSave) {
|
|
2613
2773
|
const ctx = {
|
|
2614
2774
|
modelName,
|
|
@@ -2624,6 +2784,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2624
2784
|
if (!result.allow) {
|
|
2625
2785
|
return this._errorResponse(result.reason || "Save denied", 403);
|
|
2626
2786
|
}
|
|
2787
|
+
hookAppliedNames = result.serverAppliedFieldNames;
|
|
2627
2788
|
}
|
|
2628
2789
|
const existing = this._engine.execSqlSync(
|
|
2629
2790
|
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
@@ -2647,6 +2808,11 @@ function createDatabaseDO(config = {}) {
|
|
|
2647
2808
|
}
|
|
2648
2809
|
this._engine.trackModelFields(modelName, data);
|
|
2649
2810
|
const response = { success: true, id };
|
|
2811
|
+
const appliedFields = mergeAppliedFields(
|
|
2812
|
+
collectAppliedStamps(data, autoStamps, false),
|
|
2813
|
+
collectAppliedValues(data, hookAppliedNames)
|
|
2814
|
+
);
|
|
2815
|
+
if (appliedFields) response.appliedFields = appliedFields;
|
|
2650
2816
|
return Response.json(response);
|
|
2651
2817
|
}
|
|
2652
2818
|
/** @internal */
|
|
@@ -2688,7 +2854,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2688
2854
|
/** @internal — Execute multiple save/patch/delete operations in a single transaction */
|
|
2689
2855
|
async _handleBatch(request, docId) {
|
|
2690
2856
|
const body = await request.json();
|
|
2691
|
-
const { operations } = body;
|
|
2857
|
+
const { operations, atomic = false } = body;
|
|
2692
2858
|
if (!operations || !Array.isArray(operations) || operations.length === 0) {
|
|
2693
2859
|
return this._errorResponse("operations array is required and must not be empty", 400);
|
|
2694
2860
|
}
|
|
@@ -2696,6 +2862,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2696
2862
|
const resolvedBatchIds = /* @__PURE__ */ new Map();
|
|
2697
2863
|
const opErrors = /* @__PURE__ */ new Map();
|
|
2698
2864
|
const batchCreatedIds = /* @__PURE__ */ new Set();
|
|
2865
|
+
const batchIsNew = /* @__PURE__ */ new Map();
|
|
2866
|
+
const batchAppliedNames = /* @__PURE__ */ new Map();
|
|
2699
2867
|
for (let i = 0; i < operations.length; i++) {
|
|
2700
2868
|
const op = operations[i];
|
|
2701
2869
|
if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
|
|
@@ -2769,6 +2937,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2769
2937
|
if (op.op === "save" && isNew && effectiveId) {
|
|
2770
2938
|
batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
|
|
2771
2939
|
}
|
|
2940
|
+
batchIsNew.set(i, isNew);
|
|
2772
2941
|
applyAutoStamps(op.data, op.autoStamps, isNew);
|
|
2773
2942
|
if (hooks?.beforeSave) {
|
|
2774
2943
|
const ctx = {
|
|
@@ -2784,6 +2953,8 @@ function createDatabaseDO(config = {}) {
|
|
|
2784
2953
|
const result = await hooks.beforeSave(ctx);
|
|
2785
2954
|
if (!result.allow) {
|
|
2786
2955
|
hookDenials.set(i, result.reason || "Save denied");
|
|
2956
|
+
} else if (result.serverAppliedFieldNames) {
|
|
2957
|
+
batchAppliedNames.set(i, result.serverAppliedFieldNames);
|
|
2787
2958
|
}
|
|
2788
2959
|
}
|
|
2789
2960
|
} else if (op.op === "delete") {
|
|
@@ -2832,205 +3003,263 @@ function createDatabaseDO(config = {}) {
|
|
|
2832
3003
|
}
|
|
2833
3004
|
const results = [];
|
|
2834
3005
|
const fieldsToTrack = [];
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
2857
|
-
results.push({
|
|
2858
|
-
success: false,
|
|
2859
|
-
id: conditionId,
|
|
2860
|
-
error: `Condition not met for ${op.modelName}/${conditionId}`
|
|
2861
|
-
});
|
|
2862
|
-
continue;
|
|
2863
|
-
}
|
|
2864
|
-
if (op.op === "save") {
|
|
2865
|
-
let opId = resolvedBatchIds.get(i) || op.id;
|
|
2866
|
-
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
2867
|
-
results.push({
|
|
2868
|
-
success: false,
|
|
2869
|
-
id: opId,
|
|
2870
|
-
error: `Record ${op.modelName}/${opId} already exists`
|
|
3006
|
+
const pushFailure = (index, failure) => {
|
|
3007
|
+
results.push({
|
|
3008
|
+
success: false,
|
|
3009
|
+
id: failure.id,
|
|
3010
|
+
error: failure.error,
|
|
3011
|
+
status: failure.status,
|
|
3012
|
+
code: failure.code
|
|
3013
|
+
});
|
|
3014
|
+
if (atomic) throw new AtomicBatchAbort(index);
|
|
3015
|
+
};
|
|
3016
|
+
try {
|
|
3017
|
+
this._engine.transactionSync(() => {
|
|
3018
|
+
for (let i = 0; i < operations.length; i++) {
|
|
3019
|
+
const op = operations[i];
|
|
3020
|
+
if (hookDenials.has(i)) {
|
|
3021
|
+
const effectiveId = resolvedBatchIds.get(i) || op.id || "";
|
|
3022
|
+
pushFailure(i, {
|
|
3023
|
+
id: effectiveId,
|
|
3024
|
+
error: hookDenials.get(i),
|
|
3025
|
+
status: 403,
|
|
3026
|
+
code: "HOOK_DENIED"
|
|
2871
3027
|
});
|
|
2872
3028
|
continue;
|
|
2873
3029
|
}
|
|
2874
|
-
if (
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3030
|
+
if (opErrors.has(i)) {
|
|
3031
|
+
pushFailure(i, {
|
|
3032
|
+
id: op.id || "",
|
|
3033
|
+
error: opErrors.get(i),
|
|
3034
|
+
status: 400,
|
|
3035
|
+
code: "VALIDATION_ERROR"
|
|
3036
|
+
});
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
const conditionId = resolvedBatchIds.get(i) || op.id;
|
|
3040
|
+
if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
|
|
3041
|
+
pushFailure(i, {
|
|
3042
|
+
id: conditionId,
|
|
3043
|
+
error: `Condition not met for ${op.modelName}/${conditionId}`,
|
|
3044
|
+
status: 409,
|
|
3045
|
+
code: "CONDITION_NOT_MET"
|
|
3046
|
+
});
|
|
3047
|
+
continue;
|
|
3048
|
+
}
|
|
3049
|
+
if (op.op === "save") {
|
|
3050
|
+
let opId = resolvedBatchIds.get(i) || op.id;
|
|
3051
|
+
if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
|
|
3052
|
+
pushFailure(i, {
|
|
2892
3053
|
id: opId,
|
|
2893
|
-
error:
|
|
3054
|
+
error: `Record ${op.modelName}/${opId} already exists`,
|
|
3055
|
+
status: 409,
|
|
3056
|
+
code: "ALREADY_EXISTS"
|
|
2894
3057
|
});
|
|
2895
3058
|
continue;
|
|
2896
3059
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
const dataJson = JSON.stringify(jsonFields);
|
|
2902
|
-
this._engine.execSqlSync(
|
|
2903
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2904
|
-
[opId, op.modelName, dataJson]
|
|
2905
|
-
);
|
|
2906
|
-
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
2907
|
-
this._engine.execSqlSync(
|
|
2908
|
-
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
2909
|
-
[op.modelName, opId, fieldName]
|
|
3060
|
+
if (op.data) {
|
|
3061
|
+
const existingForUnique = this._engine.execSqlSync(
|
|
3062
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3063
|
+
[opId, op.modelName]
|
|
2910
3064
|
);
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
);
|
|
3065
|
+
let dataForUnique = op.data;
|
|
3066
|
+
if (existingForUnique.length > 0) {
|
|
3067
|
+
const existingData = JSON.parse(existingForUnique[0]._data);
|
|
3068
|
+
dataForUnique = { ...existingData, ...op.data };
|
|
2916
3069
|
}
|
|
2917
|
-
}
|
|
2918
|
-
} else {
|
|
2919
|
-
const { id: _id, ...fieldsForJson } = op.data;
|
|
2920
|
-
const dataJson = JSON.stringify(fieldsForJson);
|
|
2921
|
-
this._engine.execSqlSync(
|
|
2922
|
-
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
2923
|
-
[opId, op.modelName, dataJson]
|
|
2924
|
-
);
|
|
2925
|
-
}
|
|
2926
|
-
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
2927
|
-
results.push({ success: true, id: opId });
|
|
2928
|
-
} else if (op.op === "patch") {
|
|
2929
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
2930
|
-
results.push({
|
|
2931
|
-
success: false,
|
|
2932
|
-
id: op.id,
|
|
2933
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2934
|
-
});
|
|
2935
|
-
continue;
|
|
2936
|
-
}
|
|
2937
|
-
if (op.data) {
|
|
2938
|
-
const existing = this._engine.execSqlSync(
|
|
2939
|
-
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
2940
|
-
[op.id, op.modelName]
|
|
2941
|
-
);
|
|
2942
|
-
if (existing.length > 0) {
|
|
2943
|
-
const existingData = JSON.parse(existing[0]._data);
|
|
2944
|
-
const mergedData = { ...existingData, ...op.data };
|
|
2945
3070
|
const violation = this._engine.checkUniqueConstraints(
|
|
2946
3071
|
op.modelName,
|
|
2947
|
-
|
|
2948
|
-
|
|
3072
|
+
opId,
|
|
3073
|
+
dataForUnique
|
|
2949
3074
|
);
|
|
2950
3075
|
if (violation) {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3076
|
+
pushFailure(i, {
|
|
3077
|
+
id: opId,
|
|
3078
|
+
error: violation,
|
|
3079
|
+
status: 409,
|
|
3080
|
+
code: "UNIQUE_VIOLATION"
|
|
2955
3081
|
});
|
|
2956
3082
|
continue;
|
|
2957
3083
|
}
|
|
2958
3084
|
}
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3085
|
+
if (op.stringSets && Object.keys(op.stringSets).length > 0) {
|
|
3086
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3087
|
+
const jsonFields = { ...fieldsForJson, ...op.stringSets };
|
|
3088
|
+
const dataJson = JSON.stringify(jsonFields);
|
|
3089
|
+
this._engine.execSqlSync(
|
|
3090
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3091
|
+
[opId, op.modelName, dataJson]
|
|
3092
|
+
);
|
|
3093
|
+
for (const [fieldName, values] of Object.entries(op.stringSets)) {
|
|
3094
|
+
this._engine.execSqlSync(
|
|
3095
|
+
"DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
|
|
3096
|
+
[op.modelName, opId, fieldName]
|
|
3097
|
+
);
|
|
3098
|
+
for (const value of values) {
|
|
3099
|
+
this._engine.execSqlSync(
|
|
3100
|
+
"INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
|
|
3101
|
+
[opId, op.modelName, fieldName, value]
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
} else {
|
|
3106
|
+
const { id: _id, ...fieldsForJson } = op.data;
|
|
3107
|
+
const dataJson = JSON.stringify(fieldsForJson);
|
|
3108
|
+
this._engine.execSqlSync(
|
|
3109
|
+
"INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
|
|
3110
|
+
[opId, op.modelName, dataJson]
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
2973
3113
|
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3114
|
+
const saveResult = { success: true, id: opId };
|
|
3115
|
+
const saveApplied = mergeAppliedFields(
|
|
3116
|
+
collectAppliedStamps(
|
|
3117
|
+
op.data,
|
|
3118
|
+
op.autoStamps,
|
|
3119
|
+
batchIsNew.get(i) ?? false
|
|
3120
|
+
),
|
|
3121
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3122
|
+
);
|
|
3123
|
+
if (saveApplied) saveResult.appliedFields = saveApplied;
|
|
3124
|
+
results.push(saveResult);
|
|
3125
|
+
} else if (op.op === "patch") {
|
|
3126
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3127
|
+
pushFailure(i, {
|
|
3128
|
+
id: op.id,
|
|
3129
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3130
|
+
status: 404,
|
|
3131
|
+
code: "NOT_FOUND"
|
|
3132
|
+
});
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
if (op.data) {
|
|
3136
|
+
const existing = this._engine.execSqlSync(
|
|
3137
|
+
"SELECT _data FROM records WHERE _id = ? AND _type = ?",
|
|
3138
|
+
[op.id, op.modelName]
|
|
3139
|
+
);
|
|
3140
|
+
if (existing.length > 0) {
|
|
3141
|
+
const existingData = JSON.parse(existing[0]._data);
|
|
3142
|
+
const mergedData = { ...existingData, ...op.data };
|
|
3143
|
+
const violation = this._engine.checkUniqueConstraints(
|
|
3144
|
+
op.modelName,
|
|
3145
|
+
op.id,
|
|
3146
|
+
mergedData
|
|
3147
|
+
);
|
|
3148
|
+
if (violation) {
|
|
3149
|
+
pushFailure(i, {
|
|
3150
|
+
id: op.id,
|
|
3151
|
+
error: violation,
|
|
3152
|
+
status: 409,
|
|
3153
|
+
code: "UNIQUE_VIOLATION"
|
|
3154
|
+
});
|
|
3155
|
+
continue;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const found = this._engine.patchRecordRaw(
|
|
3160
|
+
op.modelName,
|
|
3161
|
+
op.id,
|
|
3162
|
+
op.data,
|
|
3163
|
+
op.stringSets
|
|
3164
|
+
);
|
|
3165
|
+
if (!found) {
|
|
3166
|
+
pushFailure(i, {
|
|
3167
|
+
id: op.id,
|
|
3168
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3169
|
+
status: 404,
|
|
3170
|
+
code: "NOT_FOUND"
|
|
3171
|
+
});
|
|
3172
|
+
} else {
|
|
3173
|
+
if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
|
|
3174
|
+
const patchResult = {
|
|
3175
|
+
success: true,
|
|
3176
|
+
id: op.id
|
|
3177
|
+
};
|
|
3178
|
+
const patchApplied = mergeAppliedFields(
|
|
3179
|
+
collectAppliedStamps(op.data, op.autoStamps, false),
|
|
3180
|
+
collectAppliedValues(op.data, batchAppliedNames.get(i))
|
|
3181
|
+
);
|
|
3182
|
+
if (patchApplied) patchResult.appliedFields = patchApplied;
|
|
3183
|
+
results.push(patchResult);
|
|
3184
|
+
}
|
|
3185
|
+
} else if (op.op === "delete") {
|
|
3186
|
+
this._engine.execSqlSync(
|
|
3187
|
+
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
3188
|
+
[op.id, op.modelName]
|
|
3189
|
+
);
|
|
3190
|
+
this._engine.execSqlSync(
|
|
3191
|
+
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
3192
|
+
[op.id, op.modelName]
|
|
3193
|
+
);
|
|
3194
|
+
results.push({ success: true, id: op.id });
|
|
3195
|
+
} else if (op.op === "increment") {
|
|
3196
|
+
const newValues = this._engine.incrementFieldsRaw(
|
|
3197
|
+
op.modelName,
|
|
3198
|
+
op.id,
|
|
3199
|
+
op.fields
|
|
3200
|
+
);
|
|
3201
|
+
if (!newValues) {
|
|
3202
|
+
pushFailure(i, {
|
|
3203
|
+
id: op.id,
|
|
3204
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3205
|
+
status: 404,
|
|
3206
|
+
code: "NOT_FOUND"
|
|
3207
|
+
});
|
|
3208
|
+
} else {
|
|
3209
|
+
results.push({ success: true, id: op.id, values: newValues });
|
|
3210
|
+
}
|
|
3211
|
+
} else if (op.op === "addToSet") {
|
|
3212
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3213
|
+
pushFailure(i, {
|
|
3214
|
+
id: op.id,
|
|
3215
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3216
|
+
status: 404,
|
|
3217
|
+
code: "NOT_FOUND"
|
|
3218
|
+
});
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
this._engine.addToStringSetsRaw(
|
|
3222
|
+
op.modelName,
|
|
3223
|
+
op.id,
|
|
3224
|
+
op.stringSets
|
|
3225
|
+
);
|
|
3226
|
+
results.push({ success: true, id: op.id });
|
|
3227
|
+
} else if (op.op === "removeFromSet") {
|
|
3228
|
+
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3229
|
+
pushFailure(i, {
|
|
3230
|
+
id: op.id,
|
|
3231
|
+
error: `Record ${op.modelName}/${op.id} not found`,
|
|
3232
|
+
status: 404,
|
|
3233
|
+
code: "NOT_FOUND"
|
|
3234
|
+
});
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
this._engine.removeFromStringSetsRaw(
|
|
3238
|
+
op.modelName,
|
|
3239
|
+
op.id,
|
|
3240
|
+
op.stringSets
|
|
3241
|
+
);
|
|
2974
3242
|
results.push({ success: true, id: op.id });
|
|
2975
3243
|
}
|
|
2976
|
-
} else if (op.op === "delete") {
|
|
2977
|
-
this._engine.execSqlSync(
|
|
2978
|
-
"DELETE FROM records WHERE _id = ? AND _type = ?",
|
|
2979
|
-
[op.id, op.modelName]
|
|
2980
|
-
);
|
|
2981
|
-
this._engine.execSqlSync(
|
|
2982
|
-
"DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
|
|
2983
|
-
[op.id, op.modelName]
|
|
2984
|
-
);
|
|
2985
|
-
results.push({ success: true, id: op.id });
|
|
2986
|
-
} else if (op.op === "increment") {
|
|
2987
|
-
const newValues = this._engine.incrementFieldsRaw(
|
|
2988
|
-
op.modelName,
|
|
2989
|
-
op.id,
|
|
2990
|
-
op.fields
|
|
2991
|
-
);
|
|
2992
|
-
if (!newValues) {
|
|
2993
|
-
results.push({
|
|
2994
|
-
success: false,
|
|
2995
|
-
id: op.id,
|
|
2996
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
2997
|
-
});
|
|
2998
|
-
} else {
|
|
2999
|
-
results.push({ success: true, id: op.id, values: newValues });
|
|
3000
|
-
}
|
|
3001
|
-
} else if (op.op === "addToSet") {
|
|
3002
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3003
|
-
results.push({
|
|
3004
|
-
success: false,
|
|
3005
|
-
id: op.id,
|
|
3006
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3007
|
-
});
|
|
3008
|
-
continue;
|
|
3009
|
-
}
|
|
3010
|
-
this._engine.addToStringSetsRaw(
|
|
3011
|
-
op.modelName,
|
|
3012
|
-
op.id,
|
|
3013
|
-
op.stringSets
|
|
3014
|
-
);
|
|
3015
|
-
results.push({ success: true, id: op.id });
|
|
3016
|
-
} else if (op.op === "removeFromSet") {
|
|
3017
|
-
if (!this._engine.recordExists(op.modelName, op.id)) {
|
|
3018
|
-
results.push({
|
|
3019
|
-
success: false,
|
|
3020
|
-
id: op.id,
|
|
3021
|
-
error: `Record ${op.modelName}/${op.id} not found`
|
|
3022
|
-
});
|
|
3023
|
-
continue;
|
|
3024
|
-
}
|
|
3025
|
-
this._engine.removeFromStringSetsRaw(
|
|
3026
|
-
op.modelName,
|
|
3027
|
-
op.id,
|
|
3028
|
-
op.stringSets
|
|
3029
|
-
);
|
|
3030
|
-
results.push({ success: true, id: op.id });
|
|
3031
3244
|
}
|
|
3245
|
+
});
|
|
3246
|
+
} catch (err) {
|
|
3247
|
+
if (err instanceof AtomicBatchAbort) {
|
|
3248
|
+
const failed = results[err.failedIndex];
|
|
3249
|
+
const failedOp = operations[err.failedIndex];
|
|
3250
|
+
console.warn(
|
|
3251
|
+
`[js-bao batch] atomic rollback: operation ${err.failedIndex} (${failedOp?.op}) failed \u2014 ${failed?.error ?? "unknown error"}`
|
|
3252
|
+
);
|
|
3253
|
+
const errorBody = {
|
|
3254
|
+
error: `Operation ${err.failedIndex} (${failedOp?.op}) failed: ${failed?.error ?? "unknown error"}`,
|
|
3255
|
+
failedIndex: err.failedIndex,
|
|
3256
|
+
op: failedOp?.op,
|
|
3257
|
+
code: failed?.code
|
|
3258
|
+
};
|
|
3259
|
+
return Response.json(errorBody, { status: failed?.status ?? 409 });
|
|
3032
3260
|
}
|
|
3033
|
-
|
|
3261
|
+
throw err;
|
|
3262
|
+
}
|
|
3034
3263
|
for (const entry of fieldsToTrack) {
|
|
3035
3264
|
this._engine.trackModelFields(entry.modelName, entry.data);
|
|
3036
3265
|
}
|
|
@@ -3400,6 +3629,18 @@ function createDatabaseDO(config = {}) {
|
|
|
3400
3629
|
}
|
|
3401
3630
|
return facetResult;
|
|
3402
3631
|
}
|
|
3632
|
+
if (aggOptions.groupBy.length === 0) {
|
|
3633
|
+
const row = results[0];
|
|
3634
|
+
const flat = {};
|
|
3635
|
+
for (const op of aggOptions.operations) {
|
|
3636
|
+
if (op.type === "count") {
|
|
3637
|
+
flat.count = row.count;
|
|
3638
|
+
} else {
|
|
3639
|
+
flat[`${op.type}_${op.field}`] = row[`${op.type}_${op.field}`];
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
return flat;
|
|
3643
|
+
}
|
|
3403
3644
|
const nestedResult = {};
|
|
3404
3645
|
const aliasLookup = /* @__PURE__ */ new Map();
|
|
3405
3646
|
for (const entry of aliasMap) {
|
|
@@ -3727,16 +3968,7 @@ function createDatabaseDO(config = {}) {
|
|
|
3727
3968
|
}
|
|
3728
3969
|
/** @internal */
|
|
3729
3970
|
_parseRow(row) {
|
|
3730
|
-
|
|
3731
|
-
let parsed = {};
|
|
3732
|
-
if (_data) {
|
|
3733
|
-
try {
|
|
3734
|
-
parsed = JSON.parse(_data);
|
|
3735
|
-
} catch (e) {
|
|
3736
|
-
console.warn("Failed to parse _data:", e);
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
return { id: _id, type: _type, ...parsed, ...rest };
|
|
3971
|
+
return parseRecordRow(row);
|
|
3740
3972
|
}
|
|
3741
3973
|
// ─── Include (Related Data Loading) ──────────────────────────────
|
|
3742
3974
|
/** @internal — Validate an IncludeSpec, returning an error message or null */
|
|
@@ -3866,8 +4098,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3866
4098
|
spec.filter,
|
|
3867
4099
|
{ projection: spec.projection }
|
|
3868
4100
|
);
|
|
4101
|
+
const relatedProjected = spec.projection ? related.map((r) => this._applyProjection(r, spec.projection)) : related;
|
|
3869
4102
|
const lookupMap = /* @__PURE__ */ new Map();
|
|
3870
|
-
for (const r of
|
|
4103
|
+
for (const r of relatedProjected) {
|
|
3871
4104
|
lookupMap.set(r.id, r);
|
|
3872
4105
|
}
|
|
3873
4106
|
for (const rec of records) {
|
|
@@ -3900,8 +4133,9 @@ function createDatabaseDO(config = {}) {
|
|
|
3900
4133
|
spec.filter,
|
|
3901
4134
|
{ projection: spec.projection }
|
|
3902
4135
|
);
|
|
4136
|
+
const targetsProjected = spec.projection ? targets.map((t) => this._applyProjection(t, spec.projection)) : targets;
|
|
3903
4137
|
const targetLookup = /* @__PURE__ */ new Map();
|
|
3904
|
-
for (const t of
|
|
4138
|
+
for (const t of targetsProjected) {
|
|
3905
4139
|
targetLookup.set(t.id, t);
|
|
3906
4140
|
}
|
|
3907
4141
|
for (const rec of records) {
|
|
@@ -4045,6 +4279,14 @@ function createDatabaseDO(config = {}) {
|
|
|
4045
4279
|
return allResults;
|
|
4046
4280
|
}
|
|
4047
4281
|
/** @internal — Apply projection to a single record */
|
|
4282
|
+
// Apply a projection to a single record. Assumes the projection has
|
|
4283
|
+
// already been validated upstream (JsonQueryTranslator.validateProjection /
|
|
4284
|
+
// buildSelectClause reject mixed inclusion+exclusion): `isInclusion` treats
|
|
4285
|
+
// "any value === 1" as inclusion mode, so an unvalidated mixed projection
|
|
4286
|
+
// would silently ignore the excludes. `id` and `type` are system-identity
|
|
4287
|
+
// fields and are always kept in BOTH modes (a record without its `id` is
|
|
4288
|
+
// unusable downstream). `_meta` is NOT forced — an explicit `{ _meta: 0 }`
|
|
4289
|
+
// is honored.
|
|
4048
4290
|
_applyProjection(record, projection) {
|
|
4049
4291
|
const fields = Object.entries(projection);
|
|
4050
4292
|
if (fields.length === 0) return record;
|
|
@@ -4063,7 +4305,7 @@ function createDatabaseDO(config = {}) {
|
|
|
4063
4305
|
} else {
|
|
4064
4306
|
const result = { ...record };
|
|
4065
4307
|
for (const [f, v] of fields) {
|
|
4066
|
-
if (v === 0) delete result[f];
|
|
4308
|
+
if (v === 0 && f !== "id" && f !== "type") delete result[f];
|
|
4067
4309
|
}
|
|
4068
4310
|
return result;
|
|
4069
4311
|
}
|
|
@@ -4341,6 +4583,7 @@ import { parse as parseToml } from "smol-toml";
|
|
|
4341
4583
|
// src/models/schema.ts
|
|
4342
4584
|
init_BaseModel();
|
|
4343
4585
|
init_ModelRegistry();
|
|
4586
|
+
init_uniqueIndex();
|
|
4344
4587
|
function defineModelSchema(input) {
|
|
4345
4588
|
const { name, fields } = input;
|
|
4346
4589
|
const options = {
|
|
@@ -4383,7 +4626,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
|
4383
4626
|
for (const [fieldName, options] of fields.entries()) {
|
|
4384
4627
|
if (options.unique) {
|
|
4385
4628
|
resolved.push({
|
|
4386
|
-
name:
|
|
4629
|
+
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
4387
4630
|
fields: [fieldName]
|
|
4388
4631
|
});
|
|
4389
4632
|
}
|
|
@@ -4635,6 +4878,7 @@ export {
|
|
|
4635
4878
|
DurableObjectEngine,
|
|
4636
4879
|
JsonQueryTranslator,
|
|
4637
4880
|
JsonSchemaDDL,
|
|
4881
|
+
PROJECTED_JSON_ALIAS_PREFIX,
|
|
4638
4882
|
buildSafeAlias,
|
|
4639
4883
|
checkAutoStampsReservedFields,
|
|
4640
4884
|
checkReservedFieldName,
|
|
@@ -4647,6 +4891,8 @@ export {
|
|
|
4647
4891
|
handleRequest,
|
|
4648
4892
|
inferFieldType,
|
|
4649
4893
|
loadSchemaFromTomlString,
|
|
4894
|
+
parseRecordRow,
|
|
4895
|
+
projectedJsonAlias,
|
|
4650
4896
|
registerFunctionDefault,
|
|
4651
4897
|
schemaToToml,
|
|
4652
4898
|
syncInferredMeta,
|