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/browser.cjs
CHANGED
|
@@ -366,21 +366,36 @@ var init_CursorManager = __esm({
|
|
|
366
366
|
return fields;
|
|
367
367
|
}
|
|
368
368
|
/**
|
|
369
|
-
* Build ORDER BY clause from sort specification
|
|
369
|
+
* Build ORDER BY clause from sort specification.
|
|
370
|
+
*
|
|
371
|
+
* `direction` is the pagination direction (1 = forward, -1 = backward).
|
|
372
|
+
* For backward pagination each field's effective ORDER BY direction is
|
|
373
|
+
* reversed so the cursor's flipped inequality (built by
|
|
374
|
+
* `buildPaginationConditions`) returns the page adjacent to the cursor,
|
|
375
|
+
* not the globally smallest/largest window. Without this,
|
|
376
|
+
* `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
|
|
377
|
+
* rows overall instead of the n rows just below the cursor.
|
|
378
|
+
*
|
|
379
|
+
* The returned `directions` array carries the *declared* per-field sort
|
|
380
|
+
* directions (unaffected by `direction`) — `buildPaginationConditions`
|
|
381
|
+
* needs the declared directions to pick each field's comparison operator.
|
|
370
382
|
*/
|
|
371
|
-
static buildOrderClause(sort, fieldFormatter = (field) => field) {
|
|
383
|
+
static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
|
|
372
384
|
const fields = this.extractSortFields(sort);
|
|
373
385
|
const clauses = [];
|
|
374
386
|
const directions = [];
|
|
387
|
+
const sqlDir = (declared) => {
|
|
388
|
+
const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
|
|
389
|
+
return effective === 1 ? "ASC" : "DESC";
|
|
390
|
+
};
|
|
375
391
|
if (sort && Object.keys(sort).length > 0) {
|
|
376
392
|
for (const [field, dir] of Object.entries(sort)) {
|
|
377
|
-
|
|
378
|
-
clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
|
|
393
|
+
clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
|
|
379
394
|
directions.push(dir);
|
|
380
395
|
}
|
|
381
396
|
}
|
|
382
397
|
if (!sort || !sort.hasOwnProperty("id")) {
|
|
383
|
-
clauses.push(`${fieldFormatter("id")}
|
|
398
|
+
clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
|
|
384
399
|
directions.push(1);
|
|
385
400
|
} else if (sort && sort.hasOwnProperty("id")) {
|
|
386
401
|
directions.push(sort["id"]);
|
|
@@ -403,18 +418,27 @@ var init_CursorManager = __esm({
|
|
|
403
418
|
/**
|
|
404
419
|
* Generate next and previous cursors from result set
|
|
405
420
|
*/
|
|
406
|
-
static generateResultCursors(results, sortFields,
|
|
421
|
+
static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
|
|
407
422
|
if (results.length === 0) {
|
|
408
423
|
return {};
|
|
409
424
|
}
|
|
410
425
|
const cursors = {};
|
|
411
426
|
if (hasMore && results.length > 0) {
|
|
412
427
|
const lastResult = results[results.length - 1];
|
|
413
|
-
cursors.nextCursor = this.generateCursor(
|
|
428
|
+
cursors.nextCursor = this.generateCursor(
|
|
429
|
+
lastResult,
|
|
430
|
+
sortFields,
|
|
431
|
+
requestedDirection
|
|
432
|
+
);
|
|
414
433
|
}
|
|
415
434
|
if (results.length > 0 && !isFirstPage) {
|
|
416
435
|
const firstResult = results[0];
|
|
417
|
-
|
|
436
|
+
const opposite = requestedDirection === 1 ? -1 : 1;
|
|
437
|
+
cursors.prevCursor = this.generateCursor(
|
|
438
|
+
firstResult,
|
|
439
|
+
sortFields,
|
|
440
|
+
opposite
|
|
441
|
+
);
|
|
418
442
|
}
|
|
419
443
|
return cursors;
|
|
420
444
|
}
|
|
@@ -547,7 +571,8 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
547
571
|
const whereClause = this.translateFilter(filter);
|
|
548
572
|
const orderClause = CursorManager.buildOrderClause(
|
|
549
573
|
options?.sort,
|
|
550
|
-
(field) => this.getQuotedField(field)
|
|
574
|
+
(field) => this.getQuotedField(field),
|
|
575
|
+
options?.direction || 1
|
|
551
576
|
);
|
|
552
577
|
const limitClause = this.buildLimitClause(options);
|
|
553
578
|
const paginationClause = this.buildPaginationClause(
|
|
@@ -1183,9 +1208,109 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
1183
1208
|
}
|
|
1184
1209
|
});
|
|
1185
1210
|
|
|
1211
|
+
// src/utils/yjsValues.ts
|
|
1212
|
+
function isYjsComposite(value) {
|
|
1213
|
+
return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
|
|
1214
|
+
}
|
|
1215
|
+
function isStringSetYMap(value) {
|
|
1216
|
+
if (!(value instanceof Y.Map)) return false;
|
|
1217
|
+
for (const v of value.values()) {
|
|
1218
|
+
if (v !== true) return false;
|
|
1219
|
+
}
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
function normalizeYjsValue(value) {
|
|
1223
|
+
if (value instanceof Y.Text) {
|
|
1224
|
+
return value.toString();
|
|
1225
|
+
}
|
|
1226
|
+
if (value instanceof Y.Map || value instanceof Y.Array) {
|
|
1227
|
+
return value.toJSON();
|
|
1228
|
+
}
|
|
1229
|
+
return value;
|
|
1230
|
+
}
|
|
1231
|
+
function normalizeYjsValueForStorage(value) {
|
|
1232
|
+
if (value instanceof Y.Text) {
|
|
1233
|
+
return value.toString();
|
|
1234
|
+
}
|
|
1235
|
+
if (value instanceof Y.Map || value instanceof Y.Array) {
|
|
1236
|
+
return JSON.stringify(value.toJSON());
|
|
1237
|
+
}
|
|
1238
|
+
return value;
|
|
1239
|
+
}
|
|
1240
|
+
var Y;
|
|
1241
|
+
var init_yjsValues = __esm({
|
|
1242
|
+
"src/utils/yjsValues.ts"() {
|
|
1243
|
+
"use strict";
|
|
1244
|
+
Y = __toESM(require("yjs"), 1);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// src/models/writeLifecycle.ts
|
|
1249
|
+
function ensureStringSetYMap(rm, fieldName) {
|
|
1250
|
+
const nested = rm.get(fieldName);
|
|
1251
|
+
if (nested instanceof Y2.Map) return nested;
|
|
1252
|
+
const fresh = new Y2.Map();
|
|
1253
|
+
if (nested && typeof nested === "object") {
|
|
1254
|
+
for (const [member, marker] of Object.entries(
|
|
1255
|
+
nested
|
|
1256
|
+
)) {
|
|
1257
|
+
if (Boolean(marker)) fresh.set(member, true);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
rm.set(fieldName, fresh);
|
|
1261
|
+
return fresh;
|
|
1262
|
+
}
|
|
1263
|
+
function applyStringSetDelta(target, delta) {
|
|
1264
|
+
if (delta.clear) {
|
|
1265
|
+
for (const member of Array.from(target.keys())) target.delete(member);
|
|
1266
|
+
}
|
|
1267
|
+
if (delta.remove) {
|
|
1268
|
+
for (const member of delta.remove) target.delete(String(member));
|
|
1269
|
+
}
|
|
1270
|
+
if (delta.add) {
|
|
1271
|
+
for (const member of delta.add) {
|
|
1272
|
+
const key = String(member);
|
|
1273
|
+
if (target.get(key) !== true) target.set(key, true);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function applyStringSetChange(recordYMap, fieldName, change) {
|
|
1278
|
+
const target = ensureStringSetYMap(recordYMap, fieldName);
|
|
1279
|
+
applyStringSetDelta(target, {
|
|
1280
|
+
add: change.additions,
|
|
1281
|
+
remove: change.removals
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
function registerFunctionDefaultGenerator(name, fn) {
|
|
1285
|
+
FUNCTION_DEFAULT_GENERATORS.set(name, fn);
|
|
1286
|
+
}
|
|
1287
|
+
function resolveLiteralOrFunctionDefault(rawDefault) {
|
|
1288
|
+
return typeof rawDefault === "function" ? rawDefault() : rawDefault;
|
|
1289
|
+
}
|
|
1290
|
+
var Y2, import_ulid, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
|
|
1291
|
+
var init_writeLifecycle = __esm({
|
|
1292
|
+
"src/models/writeLifecycle.ts"() {
|
|
1293
|
+
"use strict";
|
|
1294
|
+
Y2 = __toESM(require("yjs"), 1);
|
|
1295
|
+
import_ulid = require("ulid");
|
|
1296
|
+
KNOWN_FIELD_TYPE_MAP = {
|
|
1297
|
+
string: true,
|
|
1298
|
+
number: true,
|
|
1299
|
+
boolean: true,
|
|
1300
|
+
date: true,
|
|
1301
|
+
id: true,
|
|
1302
|
+
stringset: true
|
|
1303
|
+
};
|
|
1304
|
+
KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
|
|
1305
|
+
FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
|
|
1306
|
+
registerFunctionDefaultGenerator("generate_ulid", () => (0, import_ulid.ulid)());
|
|
1307
|
+
UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1186
1311
|
// src/models/metaSync.ts
|
|
1187
1312
|
function inferFieldType(value) {
|
|
1188
|
-
if (value
|
|
1313
|
+
if (isStringSetYMap(value)) return "stringset";
|
|
1189
1314
|
switch (typeof value) {
|
|
1190
1315
|
case "string":
|
|
1191
1316
|
return "string";
|
|
@@ -1204,7 +1329,10 @@ function encodeDefault(value) {
|
|
|
1204
1329
|
if (value === void 0 || value === null) return void 0;
|
|
1205
1330
|
if (typeof value === "function") {
|
|
1206
1331
|
const name = KNOWN_FUNCTION_DEFAULTS.get(value);
|
|
1207
|
-
return name ? `$${name}` :
|
|
1332
|
+
return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
1335
|
+
return `$${value}`;
|
|
1208
1336
|
}
|
|
1209
1337
|
return value;
|
|
1210
1338
|
}
|
|
@@ -1230,7 +1358,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
1230
1358
|
if (compoundConstraints.length > 0) {
|
|
1231
1359
|
let constraints = meta.get("_constraints");
|
|
1232
1360
|
if (!constraints) {
|
|
1233
|
-
constraints = new
|
|
1361
|
+
constraints = new Y3.Map();
|
|
1234
1362
|
meta.set("_constraints", constraints);
|
|
1235
1363
|
}
|
|
1236
1364
|
for (const constraint of compoundConstraints) {
|
|
@@ -1241,7 +1369,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
1241
1369
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
1242
1370
|
let rels = meta.get("_relationships");
|
|
1243
1371
|
if (!rels) {
|
|
1244
|
-
rels = new
|
|
1372
|
+
rels = new Y3.Map();
|
|
1245
1373
|
meta.set("_relationships", rels);
|
|
1246
1374
|
}
|
|
1247
1375
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
@@ -1253,7 +1381,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
1253
1381
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
1254
1382
|
let fieldMeta = metaMap.get(fieldName);
|
|
1255
1383
|
if (!fieldMeta) {
|
|
1256
|
-
fieldMeta = new
|
|
1384
|
+
fieldMeta = new Y3.Map();
|
|
1257
1385
|
metaMap.set(fieldName, fieldMeta);
|
|
1258
1386
|
}
|
|
1259
1387
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -1261,6 +1389,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
1261
1389
|
if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
|
|
1262
1390
|
if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
|
|
1263
1391
|
if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
|
|
1392
|
+
if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
|
|
1264
1393
|
if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
|
|
1265
1394
|
if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
|
|
1266
1395
|
const encoded = encodeDefault(fieldOpts.default);
|
|
@@ -1269,7 +1398,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
1269
1398
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
1270
1399
|
let cMeta = constraintsMap.get(constraint.name);
|
|
1271
1400
|
if (!cMeta) {
|
|
1272
|
-
cMeta = new
|
|
1401
|
+
cMeta = new Y3.Map();
|
|
1273
1402
|
constraintsMap.set(constraint.name, cMeta);
|
|
1274
1403
|
}
|
|
1275
1404
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -1279,7 +1408,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
1279
1408
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
1280
1409
|
let relMeta = relsMap.get(relName);
|
|
1281
1410
|
if (!relMeta) {
|
|
1282
|
-
relMeta = new
|
|
1411
|
+
relMeta = new Y3.Map();
|
|
1283
1412
|
relsMap.set(relName, relMeta);
|
|
1284
1413
|
}
|
|
1285
1414
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -1294,7 +1423,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
1294
1423
|
if (fieldName.startsWith("_")) continue;
|
|
1295
1424
|
let fieldMeta = meta.get(fieldName);
|
|
1296
1425
|
if (!fieldMeta) {
|
|
1297
|
-
fieldMeta = new
|
|
1426
|
+
fieldMeta = new Y3.Map();
|
|
1298
1427
|
meta.set(fieldName, fieldMeta);
|
|
1299
1428
|
}
|
|
1300
1429
|
if (!fieldMeta.has("type")) {
|
|
@@ -1310,17 +1439,58 @@ function setIfChanged(map, key, value) {
|
|
|
1310
1439
|
map.set(key, value);
|
|
1311
1440
|
}
|
|
1312
1441
|
}
|
|
1313
|
-
var
|
|
1442
|
+
var Y3, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
|
|
1314
1443
|
var init_metaSync = __esm({
|
|
1315
1444
|
"src/models/metaSync.ts"() {
|
|
1316
1445
|
"use strict";
|
|
1317
|
-
|
|
1446
|
+
Y3 = __toESM(require("yjs"), 1);
|
|
1447
|
+
init_yjsValues();
|
|
1448
|
+
init_writeLifecycle();
|
|
1318
1449
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
1319
1450
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
1320
1451
|
}
|
|
1321
1452
|
});
|
|
1322
1453
|
|
|
1454
|
+
// src/utils/uniqueIndex.ts
|
|
1455
|
+
function buildUniqueKey(recordData, fields) {
|
|
1456
|
+
const keyParts = [];
|
|
1457
|
+
for (const field of fields) {
|
|
1458
|
+
const value = recordData instanceof Y4.Map ? recordData.get(field) : recordData[field];
|
|
1459
|
+
if (value === null || value === void 0) {
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
keyParts.push(String(value));
|
|
1463
|
+
}
|
|
1464
|
+
return keyParts.join("|");
|
|
1465
|
+
}
|
|
1466
|
+
function uniqueIndexMapName(modelName, constraintName) {
|
|
1467
|
+
return `_uniqueIdx_${modelName}_${constraintName}`;
|
|
1468
|
+
}
|
|
1469
|
+
function fieldUniqueConstraintName(modelName, fieldName) {
|
|
1470
|
+
return `${modelName}_${fieldName}_unique`;
|
|
1471
|
+
}
|
|
1472
|
+
var Y4;
|
|
1473
|
+
var init_uniqueIndex = __esm({
|
|
1474
|
+
"src/utils/uniqueIndex.ts"() {
|
|
1475
|
+
"use strict";
|
|
1476
|
+
Y4 = __toESM(require("yjs"), 1);
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1323
1480
|
// src/models/relationshipManager.ts
|
|
1481
|
+
function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
|
|
1482
|
+
if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
|
|
1483
|
+
const schema = modelClass.getSchema?.();
|
|
1484
|
+
const fields = schema?.fields;
|
|
1485
|
+
if (!fields || typeof fields.get !== "function") return;
|
|
1486
|
+
const fieldOptions = fields.get(orderByField);
|
|
1487
|
+
if (!fieldOptions) return;
|
|
1488
|
+
if (fieldOptions.required !== true) {
|
|
1489
|
+
Logger.warn(
|
|
1490
|
+
`[RelationshipManager] ${relationshipDescription} orders by '${orderByField}', which is not marked required on ${modelClass.name}. The composite (${orderByField}, id) cursor paginates deterministically, but a row whose '${orderByField}' is actually NULL has no stable cursor position and may drop from the tail. Mark '${orderByField}' as required, or order by a required field such as 'id', to remove the risk. (Full NULL-ordering support is tracked in #1316.)`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1324
1494
|
function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
|
|
1325
1495
|
return async function() {
|
|
1326
1496
|
const relatedId = this[config.relatedIdField];
|
|
@@ -1329,6 +1499,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
|
|
|
1329
1499
|
};
|
|
1330
1500
|
}
|
|
1331
1501
|
function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
|
|
1502
|
+
const {
|
|
1503
|
+
relatedIdField,
|
|
1504
|
+
orderByField = "id",
|
|
1505
|
+
orderDirection = "ASC"
|
|
1506
|
+
} = config;
|
|
1507
|
+
warnIfOrderFieldNullable(
|
|
1508
|
+
targetModelClass,
|
|
1509
|
+
orderByField,
|
|
1510
|
+
`hasMany(${targetModelClass.name})`
|
|
1511
|
+
);
|
|
1332
1512
|
return async function(paginationArgs) {
|
|
1333
1513
|
const {
|
|
1334
1514
|
limit,
|
|
@@ -1336,71 +1516,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
|
|
|
1336
1516
|
beforeCursor,
|
|
1337
1517
|
direction = "forward"
|
|
1338
1518
|
} = paginationArgs || {};
|
|
1339
|
-
const {
|
|
1340
|
-
relatedIdField,
|
|
1341
|
-
orderByField = "id",
|
|
1342
|
-
orderDirection = "ASC"
|
|
1343
|
-
} = config;
|
|
1344
1519
|
const targetSchema = targetModelClass.getSchema();
|
|
1345
1520
|
if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
|
|
1346
1521
|
Logger.error(
|
|
1347
1522
|
`[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
|
|
1348
1523
|
);
|
|
1349
|
-
return { data: [],
|
|
1524
|
+
return { data: [], hasMore: false };
|
|
1350
1525
|
}
|
|
1351
|
-
const
|
|
1526
|
+
const forward = direction !== "backward";
|
|
1527
|
+
const cursor = forward ? afterCursor : beforeCursor;
|
|
1352
1528
|
const queryOptions = {
|
|
1353
|
-
sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
|
|
1529
|
+
sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
|
|
1530
|
+
direction: forward ? 1 : -1
|
|
1354
1531
|
};
|
|
1355
|
-
if (limit !== void 0)
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
};
|
|
1362
|
-
} else if (direction === "backward") {
|
|
1363
|
-
if (beforeCursor) {
|
|
1364
|
-
filter[orderByField] = {
|
|
1365
|
-
[orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
|
|
1369
|
-
}
|
|
1370
|
-
const result = await targetModelClass.query(filter, queryOptions);
|
|
1371
|
-
const results = result.data;
|
|
1372
|
-
let nextCursor = null;
|
|
1373
|
-
let prevCursor = null;
|
|
1374
|
-
const isFirstPage = direction === "forward" && !afterCursor;
|
|
1375
|
-
if (limit !== void 0) {
|
|
1376
|
-
if (direction === "forward") {
|
|
1377
|
-
if (results.length > limit) {
|
|
1378
|
-
nextCursor = results[limit - 1][orderByField];
|
|
1379
|
-
results.pop();
|
|
1380
|
-
}
|
|
1381
|
-
if (results.length > 0 && !isFirstPage) {
|
|
1382
|
-
prevCursor = results[0][orderByField];
|
|
1383
|
-
}
|
|
1384
|
-
} else {
|
|
1385
|
-
if (results.length > limit) {
|
|
1386
|
-
prevCursor = results[limit - 1][orderByField];
|
|
1387
|
-
results.pop();
|
|
1388
|
-
}
|
|
1389
|
-
results.reverse();
|
|
1390
|
-
if (results.length > 0 && beforeCursor) {
|
|
1391
|
-
nextCursor = results[results.length - 1][orderByField];
|
|
1392
|
-
}
|
|
1393
|
-
if (results.length > 0 && !beforeCursor) {
|
|
1394
|
-
if (results.length === limit) {
|
|
1395
|
-
prevCursor = results[0][orderByField];
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
return { data: results, nextCursor, prevCursor };
|
|
1532
|
+
if (limit !== void 0) queryOptions.limit = limit;
|
|
1533
|
+
if (cursor != null) queryOptions.uniqueStartKey = cursor;
|
|
1534
|
+
return targetModelClass.query(
|
|
1535
|
+
{ [relatedIdField]: this.id },
|
|
1536
|
+
queryOptions
|
|
1537
|
+
);
|
|
1401
1538
|
};
|
|
1402
1539
|
}
|
|
1403
1540
|
function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
|
|
1541
|
+
const {
|
|
1542
|
+
joinModelLocalField,
|
|
1543
|
+
joinModelRelatedField,
|
|
1544
|
+
joinModelOrderByField = "id",
|
|
1545
|
+
joinModelOrderDirection = "ASC"
|
|
1546
|
+
} = config;
|
|
1547
|
+
warnIfOrderFieldNullable(
|
|
1548
|
+
joinModelClass,
|
|
1549
|
+
joinModelOrderByField,
|
|
1550
|
+
`hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
|
|
1551
|
+
);
|
|
1404
1552
|
return async function(paginationArgs) {
|
|
1405
1553
|
const {
|
|
1406
1554
|
limit,
|
|
@@ -1414,87 +1562,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
|
|
|
1414
1562
|
Logger.error(
|
|
1415
1563
|
`[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
|
|
1416
1564
|
);
|
|
1417
|
-
return { data: [],
|
|
1565
|
+
return { data: [], hasMore: false };
|
|
1418
1566
|
}
|
|
1419
1567
|
if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
|
|
1420
1568
|
Logger.error(
|
|
1421
1569
|
`[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
|
|
1422
1570
|
);
|
|
1423
|
-
return { data: [],
|
|
1571
|
+
return { data: [], hasMore: false };
|
|
1424
1572
|
}
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
joinModelRelatedField,
|
|
1428
|
-
joinModelOrderByField = "id",
|
|
1429
|
-
joinModelOrderDirection = "ASC"
|
|
1430
|
-
} = config;
|
|
1431
|
-
const joinFilter = { [joinModelLocalField]: this.id };
|
|
1573
|
+
const forward = direction !== "backward";
|
|
1574
|
+
const cursor = forward ? afterCursor : beforeCursor;
|
|
1432
1575
|
const joinQueryOptions = {
|
|
1433
1576
|
sort: {
|
|
1434
1577
|
[joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
|
|
1435
1578
|
},
|
|
1436
|
-
projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
|
|
1579
|
+
projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
|
|
1580
|
+
direction: forward ? 1 : -1
|
|
1437
1581
|
};
|
|
1438
|
-
if (limit !== void 0)
|
|
1439
|
-
|
|
1440
|
-
}
|
|
1441
|
-
if (direction === "forward" && afterCursor) {
|
|
1442
|
-
joinFilter[joinModelOrderByField] = {
|
|
1443
|
-
[joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
|
|
1444
|
-
};
|
|
1445
|
-
} else if (direction === "backward") {
|
|
1446
|
-
if (beforeCursor) {
|
|
1447
|
-
joinFilter[joinModelOrderByField] = {
|
|
1448
|
-
[joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
|
|
1449
|
-
};
|
|
1450
|
-
}
|
|
1451
|
-
joinQueryOptions.sort = {
|
|
1452
|
-
[joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1582
|
+
if (limit !== void 0) joinQueryOptions.limit = limit;
|
|
1583
|
+
if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
|
|
1455
1584
|
const joinResult = await joinModelClass.query(
|
|
1456
|
-
|
|
1585
|
+
{ [joinModelLocalField]: this.id },
|
|
1457
1586
|
joinQueryOptions
|
|
1458
1587
|
);
|
|
1459
|
-
const
|
|
1460
|
-
let
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
|
|
1467
|
-
joinResults.pop();
|
|
1468
|
-
}
|
|
1469
|
-
if (joinResults.length > 0 && !isFirstPage) {
|
|
1470
|
-
prevCursorFromJoin = joinResults[0][joinModelOrderByField];
|
|
1471
|
-
}
|
|
1472
|
-
} else {
|
|
1473
|
-
if (joinResults.length > limit) {
|
|
1474
|
-
prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
|
|
1475
|
-
joinResults.pop();
|
|
1476
|
-
}
|
|
1477
|
-
joinResults.reverse();
|
|
1478
|
-
if (joinResults.length > 0 && beforeCursor) {
|
|
1479
|
-
nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
|
|
1480
|
-
}
|
|
1481
|
-
if (joinResults.length > 0 && !beforeCursor) {
|
|
1482
|
-
if (joinResults.length === limit) {
|
|
1483
|
-
prevCursorFromJoin = joinResults[0][joinModelOrderByField];
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1588
|
+
const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
|
|
1589
|
+
let targetData = [];
|
|
1590
|
+
if (relatedIds.length > 0) {
|
|
1591
|
+
const targetResult = await targetModelClass.query({
|
|
1592
|
+
id: { $in: relatedIds }
|
|
1593
|
+
});
|
|
1594
|
+
targetData = targetResult.data;
|
|
1487
1595
|
}
|
|
1488
|
-
const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
|
|
1489
|
-
if (relatedIds.length === 0)
|
|
1490
|
-
return { data: [], nextCursor: null, prevCursor: null };
|
|
1491
|
-
const targetResult = await targetModelClass.query({
|
|
1492
|
-
id: { $in: relatedIds }
|
|
1493
|
-
});
|
|
1494
1596
|
return {
|
|
1495
|
-
data:
|
|
1496
|
-
nextCursor:
|
|
1497
|
-
prevCursor:
|
|
1597
|
+
data: targetData,
|
|
1598
|
+
nextCursor: joinResult.nextCursor,
|
|
1599
|
+
prevCursor: joinResult.prevCursor,
|
|
1600
|
+
hasMore: joinResult.hasMore
|
|
1498
1601
|
};
|
|
1499
1602
|
};
|
|
1500
1603
|
}
|
|
@@ -1618,10 +1721,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
|
|
|
1618
1721
|
}, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
|
|
1619
1722
|
};
|
|
1620
1723
|
}
|
|
1724
|
+
var ALWAYS_PRESENT_ORDER_FIELDS;
|
|
1621
1725
|
var init_relationshipManager = __esm({
|
|
1622
1726
|
"src/models/relationshipManager.ts"() {
|
|
1623
1727
|
"use strict";
|
|
1624
1728
|
init_BaseModel();
|
|
1729
|
+
ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
|
|
1625
1730
|
}
|
|
1626
1731
|
});
|
|
1627
1732
|
|
|
@@ -2392,7 +2497,13 @@ var init_IncludeResolver = __esm({
|
|
|
2392
2497
|
}
|
|
2393
2498
|
return null;
|
|
2394
2499
|
}
|
|
2395
|
-
/**
|
|
2500
|
+
/**
|
|
2501
|
+
* Apply a projection to a single record. Assumes the projection was already
|
|
2502
|
+
* validated upstream (mixed inclusion+exclusion rejected by the translator):
|
|
2503
|
+
* `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
|
|
2504
|
+
* are system-identity fields and are always kept in BOTH modes (mirrors the
|
|
2505
|
+
* DatabaseDO `_applyProjection`); `_meta` stays excludable.
|
|
2506
|
+
*/
|
|
2396
2507
|
_applyProjection(record, projection) {
|
|
2397
2508
|
const fields = Object.entries(projection);
|
|
2398
2509
|
if (fields.length === 0) return record;
|
|
@@ -2411,7 +2522,7 @@ var init_IncludeResolver = __esm({
|
|
|
2411
2522
|
} else {
|
|
2412
2523
|
const result = { ...record };
|
|
2413
2524
|
for (const [f, v] of fields) {
|
|
2414
|
-
if (v === 0) delete result[f];
|
|
2525
|
+
if (v === 0 && f !== "id" && f !== "type") delete result[f];
|
|
2415
2526
|
}
|
|
2416
2527
|
return result;
|
|
2417
2528
|
}
|
|
@@ -2431,20 +2542,23 @@ __export(BaseModel_exports, {
|
|
|
2431
2542
|
generateULID: () => generateULID
|
|
2432
2543
|
});
|
|
2433
2544
|
function generateULID() {
|
|
2434
|
-
return (0,
|
|
2545
|
+
return (0, import_ulid2.ulid)();
|
|
2435
2546
|
}
|
|
2436
|
-
var
|
|
2547
|
+
var Y5, import_ulid2, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
|
|
2437
2548
|
var init_BaseModel = __esm({
|
|
2438
2549
|
"src/models/BaseModel.ts"() {
|
|
2439
2550
|
"use strict";
|
|
2440
|
-
|
|
2441
|
-
|
|
2551
|
+
Y5 = __toESM(require("yjs"), 1);
|
|
2552
|
+
import_ulid2 = require("ulid");
|
|
2442
2553
|
init_StringSet();
|
|
2443
2554
|
init_documentTypes();
|
|
2444
2555
|
init_DocumentQueryTranslator();
|
|
2445
2556
|
init_CursorManager();
|
|
2446
2557
|
init_sql();
|
|
2447
2558
|
init_metaSync();
|
|
2559
|
+
init_writeLifecycle();
|
|
2560
|
+
init_yjsValues();
|
|
2561
|
+
init_uniqueIndex();
|
|
2448
2562
|
LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
2449
2563
|
LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
|
|
2450
2564
|
LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
|
|
@@ -2829,7 +2943,9 @@ var init_BaseModel = __esm({
|
|
|
2829
2943
|
this.setValue(fieldKey, data[fieldKey]);
|
|
2830
2944
|
this._constructorProvidedFields.add(fieldKey);
|
|
2831
2945
|
} else if (fieldOptions.default !== void 0) {
|
|
2832
|
-
const defaultValue =
|
|
2946
|
+
const defaultValue = resolveLiteralOrFunctionDefault(
|
|
2947
|
+
fieldOptions.default
|
|
2948
|
+
);
|
|
2833
2949
|
this.setValue(fieldKey, defaultValue);
|
|
2834
2950
|
}
|
|
2835
2951
|
}
|
|
@@ -2953,7 +3069,30 @@ var init_BaseModel = __esm({
|
|
|
2953
3069
|
if (verboseEnabled) {
|
|
2954
3070
|
Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
|
|
2955
3071
|
}
|
|
2956
|
-
return fieldValue;
|
|
3072
|
+
return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Defensive normalization for the read/sync boundary (issue #625).
|
|
3076
|
+
*
|
|
3077
|
+
* Stringset fields are owned by getStringSetFromYjs (which expects the
|
|
3078
|
+
* raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
|
|
3079
|
+
* through untouched. For any other field, a composite Yjs primitive
|
|
3080
|
+
* (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
|
|
3081
|
+
* raw: normalize it to its plain-JS projection (toJSON / toString) and
|
|
3082
|
+
* warn, rather than throwing. Scalars pass through unchanged.
|
|
3083
|
+
*/
|
|
3084
|
+
normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
|
|
3085
|
+
const fieldOptions = schema?.fields?.get(fieldKey);
|
|
3086
|
+
if (fieldOptions?.type === "stringset") {
|
|
3087
|
+
return value;
|
|
3088
|
+
}
|
|
3089
|
+
if (isYjsComposite(value)) {
|
|
3090
|
+
Logger.warn(
|
|
3091
|
+
`[${modelName ?? this.constructor.name}] Field '${fieldKey}' holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions?.type ?? "unknown"}'. Normalizing to plain JS \u2014 the writer disagreed with the schema.`
|
|
3092
|
+
);
|
|
3093
|
+
return normalizeYjsValue(value);
|
|
3094
|
+
}
|
|
3095
|
+
return value;
|
|
2957
3096
|
}
|
|
2958
3097
|
getValue(fieldKey) {
|
|
2959
3098
|
const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
|
|
@@ -3098,7 +3237,7 @@ var init_BaseModel = __esm({
|
|
|
3098
3237
|
}
|
|
3099
3238
|
getStringSetFromYjs(fieldName) {
|
|
3100
3239
|
const yjsData = this.getFromYjs(fieldName);
|
|
3101
|
-
if (yjsData instanceof
|
|
3240
|
+
if (yjsData instanceof Y5.Map) {
|
|
3102
3241
|
return Array.from(yjsData.keys());
|
|
3103
3242
|
}
|
|
3104
3243
|
if (yjsData && typeof yjsData === "object") {
|
|
@@ -3206,7 +3345,7 @@ var init_BaseModel = __esm({
|
|
|
3206
3345
|
}
|
|
3207
3346
|
const extractStringSetValues = (value) => {
|
|
3208
3347
|
if (!value) return [];
|
|
3209
|
-
if (value instanceof
|
|
3348
|
+
if (value instanceof Y5.Map) {
|
|
3210
3349
|
return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
|
|
3211
3350
|
}
|
|
3212
3351
|
if (Array.isArray(value)) {
|
|
@@ -3223,7 +3362,7 @@ var init_BaseModel = __esm({
|
|
|
3223
3362
|
if (!recordId) continue;
|
|
3224
3363
|
let itemData;
|
|
3225
3364
|
const stringSetValuesByField = {};
|
|
3226
|
-
if (recordData instanceof
|
|
3365
|
+
if (recordData instanceof Y5.Map) {
|
|
3227
3366
|
itemData = {};
|
|
3228
3367
|
const unknownFields = [];
|
|
3229
3368
|
for (const [fieldKey, value] of recordData.entries()) {
|
|
@@ -3339,20 +3478,10 @@ var init_BaseModel = __esm({
|
|
|
3339
3478
|
Logger.verbose(
|
|
3340
3479
|
`[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
|
|
3341
3480
|
);
|
|
3342
|
-
const
|
|
3343
|
-
const keyParts = [];
|
|
3344
|
-
for (const field of fields) {
|
|
3345
|
-
const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
|
|
3346
|
-
if (value === null || value === void 0) {
|
|
3347
|
-
return null;
|
|
3348
|
-
}
|
|
3349
|
-
keyParts.push(String(value));
|
|
3350
|
-
}
|
|
3351
|
-
return keyParts.join("|");
|
|
3352
|
-
};
|
|
3481
|
+
const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
|
|
3353
3482
|
const extractItemData = (key, recordData) => {
|
|
3354
3483
|
let itemData;
|
|
3355
|
-
if (recordData instanceof
|
|
3484
|
+
if (recordData instanceof Y5.Map) {
|
|
3356
3485
|
itemData = {};
|
|
3357
3486
|
const unknownFields = [];
|
|
3358
3487
|
for (const [fieldKey, value] of recordData.entries()) {
|
|
@@ -3365,7 +3494,14 @@ var init_BaseModel = __esm({
|
|
|
3365
3494
|
continue;
|
|
3366
3495
|
}
|
|
3367
3496
|
if (value !== void 0) {
|
|
3368
|
-
|
|
3497
|
+
if (isYjsComposite(value)) {
|
|
3498
|
+
Logger.warn(
|
|
3499
|
+
`[${this.name}] Field '${fieldKey}' on record ${key} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
|
|
3500
|
+
);
|
|
3501
|
+
itemData[fieldKey] = normalizeYjsValueForStorage(value);
|
|
3502
|
+
} else {
|
|
3503
|
+
itemData[fieldKey] = value;
|
|
3504
|
+
}
|
|
3369
3505
|
}
|
|
3370
3506
|
}
|
|
3371
3507
|
if (unknownFields.length > 0) {
|
|
@@ -3388,7 +3524,14 @@ var init_BaseModel = __esm({
|
|
|
3388
3524
|
continue;
|
|
3389
3525
|
}
|
|
3390
3526
|
if (value !== void 0) {
|
|
3391
|
-
|
|
3527
|
+
if (isYjsComposite(value)) {
|
|
3528
|
+
Logger.warn(
|
|
3529
|
+
`[${this.name}] Field '${fieldKey}' on legacy record ${key} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
|
|
3530
|
+
);
|
|
3531
|
+
filteredData[fieldKey] = normalizeYjsValueForStorage(value);
|
|
3532
|
+
} else {
|
|
3533
|
+
filteredData[fieldKey] = value;
|
|
3534
|
+
}
|
|
3392
3535
|
}
|
|
3393
3536
|
}
|
|
3394
3537
|
if (unknownFields.length > 0) {
|
|
@@ -3422,7 +3565,7 @@ var init_BaseModel = __esm({
|
|
|
3422
3565
|
const recordsByUniqueKey = /* @__PURE__ */ new Map();
|
|
3423
3566
|
for (const [recordId, recordData] of allRecords.entries()) {
|
|
3424
3567
|
if (recordIdsToDiscard.has(recordId)) continue;
|
|
3425
|
-
const uniqueKey =
|
|
3568
|
+
const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
|
|
3426
3569
|
if (uniqueKey === null) continue;
|
|
3427
3570
|
if (!recordsByUniqueKey.has(uniqueKey)) {
|
|
3428
3571
|
recordsByUniqueKey.set(uniqueKey, []);
|
|
@@ -3468,7 +3611,7 @@ var init_BaseModel = __esm({
|
|
|
3468
3611
|
if (!recordData || !key) continue;
|
|
3469
3612
|
const itemData = extractItemData(key, recordData);
|
|
3470
3613
|
if (!itemData) continue;
|
|
3471
|
-
if (change.action === "add" && recordData instanceof
|
|
3614
|
+
if (change.action === "add" && recordData instanceof Y5.Map) {
|
|
3472
3615
|
Logger.verbose(
|
|
3473
3616
|
`[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
|
|
3474
3617
|
);
|
|
@@ -3566,9 +3709,12 @@ var init_BaseModel = __esm({
|
|
|
3566
3709
|
const recordData = documentYMap.get(idToDiscard);
|
|
3567
3710
|
if (!recordData) continue;
|
|
3568
3711
|
for (const constraint of schema.resolvedUniqueConstraints) {
|
|
3569
|
-
const uniqueKey =
|
|
3712
|
+
const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
|
|
3570
3713
|
if (uniqueKey === null) continue;
|
|
3571
|
-
const constraintMapName =
|
|
3714
|
+
const constraintMapName = uniqueIndexMapName(
|
|
3715
|
+
modelName,
|
|
3716
|
+
constraint.name
|
|
3717
|
+
);
|
|
3572
3718
|
const constraintMap = yDoc.getMap(constraintMapName);
|
|
3573
3719
|
const currentIndexValue = constraintMap.get(uniqueKey);
|
|
3574
3720
|
if (currentIndexValue === idToDiscard) {
|
|
@@ -3586,11 +3732,14 @@ var init_BaseModel = __esm({
|
|
|
3586
3732
|
}, `conflict-resolution-${modelName}-${docId}`);
|
|
3587
3733
|
yDoc.transact(() => {
|
|
3588
3734
|
for (const constraint of schema.resolvedUniqueConstraints) {
|
|
3589
|
-
const constraintMapName =
|
|
3735
|
+
const constraintMapName = uniqueIndexMapName(
|
|
3736
|
+
modelName,
|
|
3737
|
+
constraint.name
|
|
3738
|
+
);
|
|
3590
3739
|
const constraintMap = yDoc.getMap(constraintMapName);
|
|
3591
3740
|
for (const [recordId, recordData] of documentYMap.entries()) {
|
|
3592
3741
|
if (!recordData) continue;
|
|
3593
|
-
const uniqueKey =
|
|
3742
|
+
const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
|
|
3594
3743
|
if (uniqueKey === null) continue;
|
|
3595
3744
|
const currentIndexValue = constraintMap.get(uniqueKey);
|
|
3596
3745
|
if (currentIndexValue !== recordId) {
|
|
@@ -3613,7 +3762,7 @@ var init_BaseModel = __esm({
|
|
|
3613
3762
|
`[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
|
|
3614
3763
|
);
|
|
3615
3764
|
for (const [recordId, recordData] of documentYMap.entries()) {
|
|
3616
|
-
if (recordData instanceof
|
|
3765
|
+
if (recordData instanceof Y5.Map) {
|
|
3617
3766
|
Logger.verbose(
|
|
3618
3767
|
`[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
|
|
3619
3768
|
);
|
|
@@ -3798,6 +3947,11 @@ var init_BaseModel = __esm({
|
|
|
3798
3947
|
Logger.debug(
|
|
3799
3948
|
`[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
|
|
3800
3949
|
);
|
|
3950
|
+
if (isYjsComposite(yjsValue)) {
|
|
3951
|
+
Logger.warn(
|
|
3952
|
+
`[${modelConstructor.name}] Field '${key}' holds a composite Yjs value (${yjsValue?.constructor?.name}) in a non-stringset slot; the local scalar change will overwrite it (last-writer-wins).`
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3801
3955
|
if (yjsValue === void 0) {
|
|
3802
3956
|
Logger.debug(
|
|
3803
3957
|
`[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
|
|
@@ -3834,26 +3988,7 @@ var init_BaseModel = __esm({
|
|
|
3834
3988
|
* if the field hasn't been touched since the wire-format change in #561.
|
|
3835
3989
|
*/
|
|
3836
3990
|
applyStringSetChangeToYMap(recordYMap, fieldName, change) {
|
|
3837
|
-
|
|
3838
|
-
if (!(nested instanceof Y2.Map)) {
|
|
3839
|
-
const migrated = new Y2.Map();
|
|
3840
|
-
if (nested && typeof nested === "object") {
|
|
3841
|
-
for (const [member, marker] of Object.entries(
|
|
3842
|
-
nested
|
|
3843
|
-
)) {
|
|
3844
|
-
if (Boolean(marker)) migrated.set(member, true);
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
recordYMap.set(fieldName, migrated);
|
|
3848
|
-
nested = migrated;
|
|
3849
|
-
}
|
|
3850
|
-
const target = nested;
|
|
3851
|
-
for (const member of change.additions) {
|
|
3852
|
-
target.set(member, true);
|
|
3853
|
-
}
|
|
3854
|
-
for (const member of change.removals) {
|
|
3855
|
-
target.delete(member);
|
|
3856
|
-
}
|
|
3991
|
+
applyStringSetChange(recordYMap, fieldName, change);
|
|
3857
3992
|
}
|
|
3858
3993
|
/**
|
|
3859
3994
|
* Deep equality check for comparing field values
|
|
@@ -4023,6 +4158,29 @@ var init_BaseModel = __esm({
|
|
|
4023
4158
|
);
|
|
4024
4159
|
throw new Error("Cannot save item without an id. Ensure id is set.");
|
|
4025
4160
|
}
|
|
4161
|
+
const preTransactCreateStamps = /* @__PURE__ */ new Set();
|
|
4162
|
+
{
|
|
4163
|
+
const existingRecord = targetYMap.get(this.id);
|
|
4164
|
+
const isNew = !existingRecord;
|
|
4165
|
+
for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
|
|
4166
|
+
const stamp = fieldOptions.autoStamp;
|
|
4167
|
+
if (!stamp) continue;
|
|
4168
|
+
const callerExplicit = this.hasLocalChange(fieldKey);
|
|
4169
|
+
if (callerExplicit) {
|
|
4170
|
+
const localValue = this._localChanges[fieldKey];
|
|
4171
|
+
if (localValue !== null && localValue !== void 0) continue;
|
|
4172
|
+
}
|
|
4173
|
+
if (stamp === "create") {
|
|
4174
|
+
if (!isNew) continue;
|
|
4175
|
+
const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
|
|
4176
|
+
if (persisted !== null && persisted !== void 0) continue;
|
|
4177
|
+
}
|
|
4178
|
+
this[fieldKey] = Date.now();
|
|
4179
|
+
if (stamp === "create") {
|
|
4180
|
+
preTransactCreateStamps.add(fieldKey);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4026
4184
|
this.validateBeforeSave();
|
|
4027
4185
|
Logger.debug(`[${modelName}.save] About to get current JS state`);
|
|
4028
4186
|
Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
|
|
@@ -4032,6 +4190,34 @@ var init_BaseModel = __esm({
|
|
|
4032
4190
|
`[${modelName}.save] getCurrentJSState() returned:`,
|
|
4033
4191
|
dataToSave
|
|
4034
4192
|
);
|
|
4193
|
+
const uniqueMissConflicts = /* @__PURE__ */ new Map();
|
|
4194
|
+
for (const constraint of schema.resolvedUniqueConstraints) {
|
|
4195
|
+
const newUniqueKey = this._buildUniqueKey(
|
|
4196
|
+
constraint.fields,
|
|
4197
|
+
dataToSave,
|
|
4198
|
+
modelName,
|
|
4199
|
+
constraint.name
|
|
4200
|
+
);
|
|
4201
|
+
if (newUniqueKey === null) {
|
|
4202
|
+
continue;
|
|
4203
|
+
}
|
|
4204
|
+
const constraintMap = yDoc.getMap(
|
|
4205
|
+
uniqueIndexMapName(modelName, constraint.name)
|
|
4206
|
+
);
|
|
4207
|
+
if (!constraintMap.get(newUniqueKey)) {
|
|
4208
|
+
const constraintKeyValues = constraint.fields.map(
|
|
4209
|
+
(field) => dataToSave[field]
|
|
4210
|
+
);
|
|
4211
|
+
const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
|
|
4212
|
+
constraint,
|
|
4213
|
+
constraintKeyValues,
|
|
4214
|
+
{ excludeId: this.id }
|
|
4215
|
+
);
|
|
4216
|
+
if (conflictingRecord) {
|
|
4217
|
+
uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4035
4221
|
await yDoc.transact(async () => {
|
|
4036
4222
|
if (upsertOnConstraint && upsertOnKey) {
|
|
4037
4223
|
const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
|
|
@@ -4053,6 +4239,14 @@ var init_BaseModel = __esm({
|
|
|
4053
4239
|
}
|
|
4054
4240
|
}
|
|
4055
4241
|
}
|
|
4242
|
+
for (const fieldKey of preTransactCreateStamps) {
|
|
4243
|
+
if (this._localChanges && fieldKey in this._localChanges) {
|
|
4244
|
+
delete this._localChanges[fieldKey];
|
|
4245
|
+
}
|
|
4246
|
+
if (fieldKey in dataToSave) {
|
|
4247
|
+
delete dataToSave[fieldKey];
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4056
4250
|
}
|
|
4057
4251
|
}
|
|
4058
4252
|
if (!targetYMap) {
|
|
@@ -4065,7 +4259,7 @@ var init_BaseModel = __esm({
|
|
|
4065
4259
|
Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
|
|
4066
4260
|
Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
|
|
4067
4261
|
if (!recordYMap) {
|
|
4068
|
-
recordYMap = new
|
|
4262
|
+
recordYMap = new Y5.Map();
|
|
4069
4263
|
Logger.verbose(
|
|
4070
4264
|
`[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
|
|
4071
4265
|
);
|
|
@@ -4117,7 +4311,10 @@ var init_BaseModel = __esm({
|
|
|
4117
4311
|
);
|
|
4118
4312
|
continue;
|
|
4119
4313
|
}
|
|
4120
|
-
const constraintMapName =
|
|
4314
|
+
const constraintMapName = uniqueIndexMapName(
|
|
4315
|
+
modelName,
|
|
4316
|
+
constraint.name
|
|
4317
|
+
);
|
|
4121
4318
|
const constraintMap = yDoc.getMap(constraintMapName);
|
|
4122
4319
|
Logger.verbose(
|
|
4123
4320
|
`[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
|
|
@@ -4146,6 +4343,26 @@ var init_BaseModel = __esm({
|
|
|
4146
4343
|
this.id,
|
|
4147
4344
|
existingRecordIdWithNewKey
|
|
4148
4345
|
);
|
|
4346
|
+
} else if (!existingRecordIdWithNewKey) {
|
|
4347
|
+
const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
|
|
4348
|
+
if (conflictingRecordId && conflictingRecordId !== this.id) {
|
|
4349
|
+
Logger.verbose(
|
|
4350
|
+
`[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
|
|
4351
|
+
);
|
|
4352
|
+
throw new UniqueConstraintViolationError(
|
|
4353
|
+
`Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
|
|
4354
|
+
", "
|
|
4355
|
+
)}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
|
|
4356
|
+
0,
|
|
4357
|
+
50
|
|
4358
|
+
)}...' already exist for index-less record ID ${conflictingRecordId}.`,
|
|
4359
|
+
modelName,
|
|
4360
|
+
constraint.name,
|
|
4361
|
+
constraint.fields,
|
|
4362
|
+
this.id,
|
|
4363
|
+
conflictingRecordId
|
|
4364
|
+
);
|
|
4365
|
+
}
|
|
4149
4366
|
}
|
|
4150
4367
|
}
|
|
4151
4368
|
if (isUpdate && oldData) {
|
|
@@ -4489,34 +4706,46 @@ var init_BaseModel = __esm({
|
|
|
4489
4706
|
const modelName = schema.options.name;
|
|
4490
4707
|
Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
|
|
4491
4708
|
const hasIncludes = options?.include && options.include.length > 0;
|
|
4492
|
-
const
|
|
4709
|
+
const requestedLimit = options?.limit;
|
|
4710
|
+
const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
|
|
4711
|
+
let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
|
|
4712
|
+
if (overFetch) {
|
|
4713
|
+
translatorOptions = {
|
|
4714
|
+
...translatorOptions || {},
|
|
4715
|
+
limit: requestedLimit + 1
|
|
4716
|
+
};
|
|
4717
|
+
}
|
|
4493
4718
|
const translator = new DocumentQueryTranslator(modelName, schema.fields);
|
|
4494
4719
|
const translatedQuery = translator.translateFind(filter, translatorOptions);
|
|
4495
4720
|
Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
|
|
4496
4721
|
Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
|
|
4497
|
-
|
|
4722
|
+
let results = await _BaseModel.dbInstance.query(
|
|
4498
4723
|
translatedQuery.sql,
|
|
4499
4724
|
translatedQuery.params
|
|
4500
4725
|
);
|
|
4501
4726
|
Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
|
|
4727
|
+
let hasMore = false;
|
|
4728
|
+
if (overFetch && results.length > requestedLimit) {
|
|
4729
|
+
hasMore = true;
|
|
4730
|
+
results = results.slice(0, requestedLimit);
|
|
4731
|
+
}
|
|
4502
4732
|
if (options?.include && options.include.length > 0) {
|
|
4503
4733
|
const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
|
|
4504
4734
|
const resolver = new IncludeResolver2(_BaseModel.dbInstance);
|
|
4505
4735
|
await resolver.resolve(results, options.include, 0, modelName);
|
|
4506
4736
|
}
|
|
4507
|
-
const requestedLimit = options?.limit;
|
|
4508
|
-
const hasMore = CursorManager.hasMoreResults(
|
|
4509
|
-
requestedLimit,
|
|
4510
|
-
results.length
|
|
4511
|
-
);
|
|
4512
4737
|
const isFirstPage = !options?.uniqueStartKey;
|
|
4738
|
+
const direction = options?.direction || 1;
|
|
4513
4739
|
const cursors = CursorManager.generateResultCursors(
|
|
4514
4740
|
results,
|
|
4515
4741
|
translatedQuery.sortFields,
|
|
4516
|
-
|
|
4742
|
+
direction,
|
|
4517
4743
|
hasMore,
|
|
4518
4744
|
isFirstPage
|
|
4519
4745
|
);
|
|
4746
|
+
if (direction === -1) {
|
|
4747
|
+
results.reverse();
|
|
4748
|
+
}
|
|
4520
4749
|
let transformedData;
|
|
4521
4750
|
if (options?.projection && Object.keys(options.projection).length > 0) {
|
|
4522
4751
|
transformedData = results.map((row) => {
|
|
@@ -5092,7 +5321,7 @@ var init_BaseModel = __esm({
|
|
|
5092
5321
|
);
|
|
5093
5322
|
}
|
|
5094
5323
|
for (const [recordId, recordYMap] of documentYMap.entries()) {
|
|
5095
|
-
if (recordYMap instanceof
|
|
5324
|
+
if (recordYMap instanceof Y5.Map) {
|
|
5096
5325
|
const instance = new this({ id: recordId });
|
|
5097
5326
|
instance._metaDocId = docId;
|
|
5098
5327
|
const connectedDoc = modelConstructor.connectedDocuments.get(docId);
|
|
@@ -5117,6 +5346,50 @@ var init_BaseModel = __esm({
|
|
|
5117
5346
|
}
|
|
5118
5347
|
return allInstances;
|
|
5119
5348
|
}
|
|
5349
|
+
/**
|
|
5350
|
+
* Resolve a record by its unique-constraint field values via an engine query.
|
|
5351
|
+
* Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
|
|
5352
|
+
* entry for a key.
|
|
5353
|
+
*
|
|
5354
|
+
* The index map is a cache over the SQLite projection, not the source of
|
|
5355
|
+
* truth: every record is loaded into the projection on `initializeForDocument`
|
|
5356
|
+
* regardless of index presence, but a record written by a client predating
|
|
5357
|
+
* index maintenance (legacy data) is missing from the index. Such a record
|
|
5358
|
+
* silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
|
|
5359
|
+
* check. A query over the constraint fields finds it reliably.
|
|
5360
|
+
*
|
|
5361
|
+
* Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
|
|
5362
|
+
* the document that holds it, via `queryOne`) or `null`. Callers decide
|
|
5363
|
+
* whether to heal the missing index entry (write paths) and do their own
|
|
5364
|
+
* logging — this method never mutates the index, so read paths stay
|
|
5365
|
+
* read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
|
|
5366
|
+
* constraint field means the record does not participate in the constraint,
|
|
5367
|
+
* so no query runs.
|
|
5368
|
+
*/
|
|
5369
|
+
static async _resolveByUniqueFallback(constraint, keyValues, options) {
|
|
5370
|
+
if (!_BaseModel.dbInstance) {
|
|
5371
|
+
return null;
|
|
5372
|
+
}
|
|
5373
|
+
const filter = {};
|
|
5374
|
+
for (let i = 0; i < constraint.fields.length; i++) {
|
|
5375
|
+
const value = keyValues[i];
|
|
5376
|
+
if (value === null || value === void 0) {
|
|
5377
|
+
return null;
|
|
5378
|
+
}
|
|
5379
|
+
filter[constraint.fields[i]] = value;
|
|
5380
|
+
}
|
|
5381
|
+
if (options?.excludeId) {
|
|
5382
|
+
if ("id" in filter) {
|
|
5383
|
+
if (filter.id === options.excludeId) {
|
|
5384
|
+
return null;
|
|
5385
|
+
}
|
|
5386
|
+
} else {
|
|
5387
|
+
filter.id = { $ne: options.excludeId };
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
const found = await this.queryOne(filter);
|
|
5391
|
+
return found ?? null;
|
|
5392
|
+
}
|
|
5120
5393
|
static async findByUnique(constraintName, value) {
|
|
5121
5394
|
const modelConstructor = this;
|
|
5122
5395
|
const schema = modelConstructor.getSchema();
|
|
@@ -5155,7 +5428,10 @@ var init_BaseModel = __esm({
|
|
|
5155
5428
|
const documentInfo = modelConstructor.connectedDocuments.get(docId);
|
|
5156
5429
|
if (!documentInfo) continue;
|
|
5157
5430
|
const yDoc = documentInfo.yDoc;
|
|
5158
|
-
const constraintMapName2 =
|
|
5431
|
+
const constraintMapName2 = uniqueIndexMapName(
|
|
5432
|
+
currentModelName,
|
|
5433
|
+
constraint.name
|
|
5434
|
+
);
|
|
5159
5435
|
const constraintMap2 = yDoc.getMap(constraintMapName2);
|
|
5160
5436
|
const recordId2 = constraintMap2.get(uniqueKeyString);
|
|
5161
5437
|
if (recordId2) {
|
|
@@ -5163,7 +5439,7 @@ var init_BaseModel = __esm({
|
|
|
5163
5439
|
const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
|
|
5164
5440
|
if (documentYMap) {
|
|
5165
5441
|
const recordYMap2 = documentYMap.get(recordId2);
|
|
5166
|
-
if (recordYMap2 && recordYMap2 instanceof
|
|
5442
|
+
if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
|
|
5167
5443
|
const instance = new modelConstructor({
|
|
5168
5444
|
id: recordId2
|
|
5169
5445
|
});
|
|
@@ -5177,6 +5453,16 @@ var init_BaseModel = __esm({
|
|
|
5177
5453
|
}
|
|
5178
5454
|
}
|
|
5179
5455
|
}
|
|
5456
|
+
const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
|
|
5457
|
+
constraint,
|
|
5458
|
+
keyValues
|
|
5459
|
+
);
|
|
5460
|
+
if (fallbackRecord) {
|
|
5461
|
+
Logger.verbose(
|
|
5462
|
+
`[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
|
|
5463
|
+
);
|
|
5464
|
+
return fallbackRecord;
|
|
5465
|
+
}
|
|
5180
5466
|
Logger.verbose(
|
|
5181
5467
|
`[${currentModelName}.findByUnique] Record not found in any connected document`
|
|
5182
5468
|
);
|
|
@@ -5190,17 +5476,30 @@ var init_BaseModel = __esm({
|
|
|
5190
5476
|
`[${currentModelName}] No Y.Doc is connected for this model. Connect a document via initJsBao(...).connectDocument or call initializeForDocument(yDoc, db, docId, permissionHint) before using legacy findByUnique.`
|
|
5191
5477
|
);
|
|
5192
5478
|
}
|
|
5193
|
-
const constraintMapName =
|
|
5479
|
+
const constraintMapName = uniqueIndexMapName(
|
|
5480
|
+
currentModelName,
|
|
5481
|
+
constraint.name
|
|
5482
|
+
);
|
|
5194
5483
|
const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
|
|
5195
5484
|
const recordId = constraintMap.get(uniqueKeyString);
|
|
5196
5485
|
if (!recordId) {
|
|
5486
|
+
const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
|
|
5487
|
+
constraint,
|
|
5488
|
+
keyValues
|
|
5489
|
+
);
|
|
5490
|
+
if (fallbackRecord) {
|
|
5491
|
+
Logger.verbose(
|
|
5492
|
+
`[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
|
|
5493
|
+
);
|
|
5494
|
+
return fallbackRecord;
|
|
5495
|
+
}
|
|
5197
5496
|
return null;
|
|
5198
5497
|
}
|
|
5199
5498
|
const recordYMap = legacyDocumentMap.get(recordId);
|
|
5200
5499
|
if (!recordYMap) {
|
|
5201
5500
|
return null;
|
|
5202
5501
|
}
|
|
5203
|
-
if (recordYMap instanceof
|
|
5502
|
+
if (recordYMap instanceof Y5.Map) {
|
|
5204
5503
|
const instance = new modelConstructor({
|
|
5205
5504
|
id: recordId
|
|
5206
5505
|
});
|
|
@@ -5258,7 +5557,10 @@ var init_BaseModel = __esm({
|
|
|
5258
5557
|
`[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
|
|
5259
5558
|
);
|
|
5260
5559
|
}
|
|
5261
|
-
const constraintMapName =
|
|
5560
|
+
const constraintMapName = uniqueIndexMapName(
|
|
5561
|
+
currentModelName,
|
|
5562
|
+
constraint.name
|
|
5563
|
+
);
|
|
5262
5564
|
let existingRecord = null;
|
|
5263
5565
|
if (connectedDocuments && connectedDocuments.size > 0) {
|
|
5264
5566
|
for (const docId of connectedDocuments.keys()) {
|
|
@@ -5276,6 +5578,33 @@ var init_BaseModel = __esm({
|
|
|
5276
5578
|
}
|
|
5277
5579
|
} else {
|
|
5278
5580
|
}
|
|
5581
|
+
if (!existingRecord) {
|
|
5582
|
+
const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
|
|
5583
|
+
constraint,
|
|
5584
|
+
keyValuesForLookup
|
|
5585
|
+
);
|
|
5586
|
+
if (fallbackRecord) {
|
|
5587
|
+
existingRecord = fallbackRecord;
|
|
5588
|
+
const healDocId = fallbackRecord._metaDocId;
|
|
5589
|
+
const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
|
|
5590
|
+
const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
|
|
5591
|
+
if (healDocInfo && canHeal) {
|
|
5592
|
+
healDocInfo.yDoc.transact(() => {
|
|
5593
|
+
const healMap = healDocInfo.yDoc.getMap(constraintMapName);
|
|
5594
|
+
if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
|
|
5595
|
+
healMap.set(uniqueKeyString, fallbackRecord.id);
|
|
5596
|
+
}
|
|
5597
|
+
}, `heal-unique-index-${currentModelName}-${healDocId}`);
|
|
5598
|
+
Logger.verbose(
|
|
5599
|
+
`[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
|
|
5600
|
+
);
|
|
5601
|
+
} else {
|
|
5602
|
+
Logger.verbose(
|
|
5603
|
+
`[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; skipped index heal (owning document is read-only or not connected).`
|
|
5604
|
+
);
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5279
5608
|
if (options?.objectMustExist) {
|
|
5280
5609
|
if (!existingRecord) {
|
|
5281
5610
|
throw new RecordNotFoundError(
|
|
@@ -5432,7 +5761,7 @@ var init_BaseModel = __esm({
|
|
|
5432
5761
|
for (const [fieldKey, fieldOptions] of schema.fields) {
|
|
5433
5762
|
if (fieldOptions?.type !== "stringset") continue;
|
|
5434
5763
|
const value = recordYMap.get(fieldKey);
|
|
5435
|
-
if (value instanceof
|
|
5764
|
+
if (value instanceof Y5.Map) {
|
|
5436
5765
|
this.observeStringSetMapOnce(value);
|
|
5437
5766
|
}
|
|
5438
5767
|
}
|
|
@@ -5479,7 +5808,14 @@ var init_BaseModel = __esm({
|
|
|
5479
5808
|
continue;
|
|
5480
5809
|
}
|
|
5481
5810
|
if (value !== void 0) {
|
|
5482
|
-
|
|
5811
|
+
if (isYjsComposite(value)) {
|
|
5812
|
+
Logger.warn(
|
|
5813
|
+
`[${modelName}] Field '${key}' on record ${recordId} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
|
|
5814
|
+
);
|
|
5815
|
+
updatedData[key] = normalizeYjsValueForStorage(value);
|
|
5816
|
+
} else {
|
|
5817
|
+
updatedData[key] = value;
|
|
5818
|
+
}
|
|
5483
5819
|
}
|
|
5484
5820
|
}
|
|
5485
5821
|
if (unknownFields.length > 0) {
|
|
@@ -5559,7 +5895,14 @@ var init_BaseModel = __esm({
|
|
|
5559
5895
|
continue;
|
|
5560
5896
|
}
|
|
5561
5897
|
if (value !== void 0) {
|
|
5562
|
-
|
|
5898
|
+
if (isYjsComposite(value)) {
|
|
5899
|
+
Logger.warn(
|
|
5900
|
+
`[${modelName}] Field '${key}' on record ${recordId} in document ${docId} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
|
|
5901
|
+
);
|
|
5902
|
+
updatedData[key] = normalizeYjsValueForStorage(value);
|
|
5903
|
+
} else {
|
|
5904
|
+
updatedData[key] = value;
|
|
5905
|
+
}
|
|
5563
5906
|
}
|
|
5564
5907
|
}
|
|
5565
5908
|
if (unknownFields.length > 0) {
|
|
@@ -5685,7 +6028,144 @@ var import_sql4 = __toESM(require("sql.js"), 1);
|
|
|
5685
6028
|
init_BaseModel();
|
|
5686
6029
|
var import_async_mutex = require("async-mutex");
|
|
5687
6030
|
init_sql();
|
|
6031
|
+
|
|
6032
|
+
// src/utils/environment.ts
|
|
6033
|
+
function detectEnvironment() {
|
|
6034
|
+
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
6035
|
+
return "node";
|
|
6036
|
+
}
|
|
6037
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
6038
|
+
return "browser";
|
|
6039
|
+
}
|
|
6040
|
+
if (typeof self !== "undefined" && typeof self.importScripts === "function") {
|
|
6041
|
+
return "browser";
|
|
6042
|
+
}
|
|
6043
|
+
return "unknown";
|
|
6044
|
+
}
|
|
6045
|
+
function isNode() {
|
|
6046
|
+
return detectEnvironment() === "node";
|
|
6047
|
+
}
|
|
6048
|
+
function isBrowser() {
|
|
6049
|
+
return detectEnvironment() === "browser";
|
|
6050
|
+
}
|
|
6051
|
+
async function isNodeModuleAvailable(moduleName) {
|
|
6052
|
+
if (!isNode()) {
|
|
6053
|
+
return false;
|
|
6054
|
+
}
|
|
6055
|
+
try {
|
|
6056
|
+
await import(
|
|
6057
|
+
/* @vite-ignore */
|
|
6058
|
+
moduleName
|
|
6059
|
+
);
|
|
6060
|
+
return true;
|
|
6061
|
+
} catch {
|
|
6062
|
+
return false;
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
var features = {
|
|
6066
|
+
get hasWebWorkers() {
|
|
6067
|
+
return isBrowser() && typeof Worker !== "undefined";
|
|
6068
|
+
},
|
|
6069
|
+
get hasFileSystem() {
|
|
6070
|
+
return isNode();
|
|
6071
|
+
},
|
|
6072
|
+
get hasWebAssembly() {
|
|
6073
|
+
return typeof WebAssembly !== "undefined";
|
|
6074
|
+
},
|
|
6075
|
+
async hasBetterSqlite3() {
|
|
6076
|
+
return await isNodeModuleAvailable("better-sqlite3");
|
|
6077
|
+
}
|
|
6078
|
+
};
|
|
6079
|
+
|
|
6080
|
+
// src/engines/SqljsEngine.ts
|
|
6081
|
+
var import_meta = {};
|
|
5688
6082
|
var SQL_WASM_URL = "/sql-wasm.wasm";
|
|
6083
|
+
var SQL_WASM_FILENAME = "sql-wasm.wasm";
|
|
6084
|
+
async function resolveNodeWasmPath() {
|
|
6085
|
+
if (!isNode()) {
|
|
6086
|
+
return null;
|
|
6087
|
+
}
|
|
6088
|
+
try {
|
|
6089
|
+
const cjsRequire = typeof require !== "undefined" ? require : void 0;
|
|
6090
|
+
if (cjsRequire) {
|
|
6091
|
+
return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
|
|
6092
|
+
}
|
|
6093
|
+
} catch {
|
|
6094
|
+
}
|
|
6095
|
+
try {
|
|
6096
|
+
const metaUrl = import_meta.url;
|
|
6097
|
+
if (metaUrl) {
|
|
6098
|
+
const { createRequire } = await import(
|
|
6099
|
+
/* @vite-ignore */
|
|
6100
|
+
"module"
|
|
6101
|
+
);
|
|
6102
|
+
const req = createRequire(metaUrl);
|
|
6103
|
+
return req.resolve("sql.js/dist/sql-wasm.wasm");
|
|
6104
|
+
}
|
|
6105
|
+
} catch {
|
|
6106
|
+
}
|
|
6107
|
+
return null;
|
|
6108
|
+
}
|
|
6109
|
+
async function preflightNodeWasm(wasmPath) {
|
|
6110
|
+
if (!isNode()) {
|
|
6111
|
+
return;
|
|
6112
|
+
}
|
|
6113
|
+
if (/^[a-z]+:\/\//i.test(wasmPath)) {
|
|
6114
|
+
return;
|
|
6115
|
+
}
|
|
6116
|
+
const { existsSync } = await import(
|
|
6117
|
+
/* @vite-ignore */
|
|
6118
|
+
"fs"
|
|
6119
|
+
);
|
|
6120
|
+
if (!existsSync(wasmPath)) {
|
|
6121
|
+
throw new Error(
|
|
6122
|
+
`[SqljsEngine] sql.js wasm not found at "${wasmPath}". Pass \`locateFile\`/\`wasmURL\` pointing at sql-wasm.wasm, or install \`better-sqlite3\` and use the \`node-sqlite\` engine (see getRecommendedNodeEngine()).`
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
async function loadSqlJsModule(locateFile) {
|
|
6127
|
+
let abortReject;
|
|
6128
|
+
const toError = (reason) => reason instanceof Error ? reason : new Error(
|
|
6129
|
+
`[SqljsEngine] sql.js aborted during initialization: ${String(
|
|
6130
|
+
reason
|
|
6131
|
+
)}`
|
|
6132
|
+
);
|
|
6133
|
+
const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
|
|
6134
|
+
const onUnhandled = (reason) => {
|
|
6135
|
+
const err = toError(reason);
|
|
6136
|
+
if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
|
|
6137
|
+
abortReject?.(err);
|
|
6138
|
+
return;
|
|
6139
|
+
}
|
|
6140
|
+
if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
|
|
6141
|
+
nodeProcess.removeListener("unhandledRejection", onUnhandled);
|
|
6142
|
+
void Promise.reject(reason);
|
|
6143
|
+
}
|
|
6144
|
+
};
|
|
6145
|
+
if (nodeProcess) {
|
|
6146
|
+
nodeProcess.on("unhandledRejection", onUnhandled);
|
|
6147
|
+
}
|
|
6148
|
+
const abortGuard = new Promise((_resolve, reject) => {
|
|
6149
|
+
abortReject = reject;
|
|
6150
|
+
});
|
|
6151
|
+
abortGuard.catch(() => {
|
|
6152
|
+
});
|
|
6153
|
+
try {
|
|
6154
|
+
const initPromise = (0, import_sql4.default)({
|
|
6155
|
+
locateFile,
|
|
6156
|
+
// Layer 1: emscripten onAbort.
|
|
6157
|
+
onAbort: (reason) => abortReject?.(toError(reason))
|
|
6158
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6159
|
+
});
|
|
6160
|
+
return await Promise.race([initPromise, abortGuard]);
|
|
6161
|
+
} finally {
|
|
6162
|
+
if (nodeProcess) {
|
|
6163
|
+
setTimeout(() => {
|
|
6164
|
+
nodeProcess.removeListener("unhandledRejection", onUnhandled);
|
|
6165
|
+
}, 0);
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
5689
6169
|
var TransactionalSQLJSOperations = class {
|
|
5690
6170
|
db;
|
|
5691
6171
|
engine;
|
|
@@ -5751,22 +6231,37 @@ var SqljsEngine = class _SqljsEngine {
|
|
|
5751
6231
|
}
|
|
5752
6232
|
async initialize(engineOptions) {
|
|
5753
6233
|
Logger.info("[SqljsEngine] Initializing SQL.js engine...");
|
|
6234
|
+
const loadingModuleThisCall = !_SqljsEngine.SQL;
|
|
5754
6235
|
try {
|
|
5755
6236
|
if (!_SqljsEngine.SQL) {
|
|
5756
|
-
|
|
6237
|
+
let locateFileConfig;
|
|
6238
|
+
if (engineOptions?.locateFile) {
|
|
6239
|
+
locateFileConfig = engineOptions.locateFile;
|
|
6240
|
+
} else if (engineOptions?.wasmURL) {
|
|
6241
|
+
const url = engineOptions.wasmURL;
|
|
6242
|
+
locateFileConfig = (_file) => url;
|
|
6243
|
+
} else {
|
|
6244
|
+
const nodeWasmPath = await resolveNodeWasmPath();
|
|
6245
|
+
const resolved = nodeWasmPath ?? SQL_WASM_URL;
|
|
6246
|
+
locateFileConfig = (_file) => resolved;
|
|
6247
|
+
}
|
|
6248
|
+
const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
|
|
5757
6249
|
Logger.debug(
|
|
5758
6250
|
"[SqljsEngine] Attempting to load SQL.js WASM from:",
|
|
5759
|
-
|
|
6251
|
+
wasmPath
|
|
5760
6252
|
);
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
6253
|
+
await preflightNodeWasm(wasmPath);
|
|
6254
|
+
const SQL = await loadSqlJsModule(locateFileConfig);
|
|
6255
|
+
_SqljsEngine.SQL = SQL;
|
|
5764
6256
|
Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
|
|
5765
6257
|
}
|
|
5766
6258
|
this.db = new _SqljsEngine.SQL.Database();
|
|
5767
6259
|
Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
|
|
5768
6260
|
this.updateTableNames();
|
|
5769
6261
|
} catch (error) {
|
|
6262
|
+
if (loadingModuleThisCall) {
|
|
6263
|
+
_SqljsEngine.SQL = null;
|
|
6264
|
+
}
|
|
5770
6265
|
Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
|
|
5771
6266
|
throw error;
|
|
5772
6267
|
}
|
|
@@ -6052,54 +6547,6 @@ var SqljsEngine = class _SqljsEngine {
|
|
|
6052
6547
|
}
|
|
6053
6548
|
};
|
|
6054
6549
|
|
|
6055
|
-
// src/utils/environment.ts
|
|
6056
|
-
function detectEnvironment() {
|
|
6057
|
-
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
6058
|
-
return "node";
|
|
6059
|
-
}
|
|
6060
|
-
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
6061
|
-
return "browser";
|
|
6062
|
-
}
|
|
6063
|
-
if (typeof self !== "undefined" && typeof self.importScripts === "function") {
|
|
6064
|
-
return "browser";
|
|
6065
|
-
}
|
|
6066
|
-
return "unknown";
|
|
6067
|
-
}
|
|
6068
|
-
function isNode() {
|
|
6069
|
-
return detectEnvironment() === "node";
|
|
6070
|
-
}
|
|
6071
|
-
function isBrowser() {
|
|
6072
|
-
return detectEnvironment() === "browser";
|
|
6073
|
-
}
|
|
6074
|
-
async function isNodeModuleAvailable(moduleName) {
|
|
6075
|
-
if (!isNode()) {
|
|
6076
|
-
return false;
|
|
6077
|
-
}
|
|
6078
|
-
try {
|
|
6079
|
-
await import(
|
|
6080
|
-
/* @vite-ignore */
|
|
6081
|
-
moduleName
|
|
6082
|
-
);
|
|
6083
|
-
return true;
|
|
6084
|
-
} catch {
|
|
6085
|
-
return false;
|
|
6086
|
-
}
|
|
6087
|
-
}
|
|
6088
|
-
var features = {
|
|
6089
|
-
get hasWebWorkers() {
|
|
6090
|
-
return isBrowser() && typeof Worker !== "undefined";
|
|
6091
|
-
},
|
|
6092
|
-
get hasFileSystem() {
|
|
6093
|
-
return isNode();
|
|
6094
|
-
},
|
|
6095
|
-
get hasWebAssembly() {
|
|
6096
|
-
return typeof WebAssembly !== "undefined";
|
|
6097
|
-
},
|
|
6098
|
-
async hasBetterSqlite3() {
|
|
6099
|
-
return await isNodeModuleAvailable("better-sqlite3");
|
|
6100
|
-
}
|
|
6101
|
-
};
|
|
6102
|
-
|
|
6103
6550
|
// src/engines/BrowserDatabaseFactory.ts
|
|
6104
6551
|
init_BaseModel();
|
|
6105
6552
|
var BrowserDatabaseFactory = class {
|
|
@@ -6410,6 +6857,7 @@ init_StringSet();
|
|
|
6410
6857
|
// src/models/schema.ts
|
|
6411
6858
|
init_BaseModel();
|
|
6412
6859
|
init_ModelRegistry();
|
|
6860
|
+
init_uniqueIndex();
|
|
6413
6861
|
function defineModelSchema(input) {
|
|
6414
6862
|
const { name, fields } = input;
|
|
6415
6863
|
const options = {
|
|
@@ -6482,7 +6930,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
|
|
|
6482
6930
|
for (const [fieldName, options] of fields.entries()) {
|
|
6483
6931
|
if (options.unique) {
|
|
6484
6932
|
resolved.push({
|
|
6485
|
-
name:
|
|
6933
|
+
name: fieldUniqueConstraintName(modelName, fieldName),
|
|
6486
6934
|
fields: [fieldName]
|
|
6487
6935
|
});
|
|
6488
6936
|
}
|
|
@@ -6546,7 +6994,8 @@ function attachAndRegisterModel(modelClass, schema) {
|
|
|
6546
6994
|
}
|
|
6547
6995
|
|
|
6548
6996
|
// src/utils/yDocSchema.ts
|
|
6549
|
-
var
|
|
6997
|
+
var Y6 = __toESM(require("yjs"), 1);
|
|
6998
|
+
init_yjsValues();
|
|
6550
6999
|
function discoverSchema(yDoc) {
|
|
6551
7000
|
const models = {};
|
|
6552
7001
|
const metaNames = /* @__PURE__ */ new Set();
|
|
@@ -6580,7 +7029,7 @@ function discoverModelNames(yDoc) {
|
|
|
6580
7029
|
function materializeMap(yDoc, key) {
|
|
6581
7030
|
try {
|
|
6582
7031
|
const map = yDoc.getMap(key);
|
|
6583
|
-
return map instanceof
|
|
7032
|
+
return map instanceof Y6.Map ? map : null;
|
|
6584
7033
|
} catch {
|
|
6585
7034
|
return null;
|
|
6586
7035
|
}
|
|
@@ -6590,11 +7039,11 @@ function readModelMeta(metaMap) {
|
|
|
6590
7039
|
let constraints;
|
|
6591
7040
|
let relationships;
|
|
6592
7041
|
for (const [key, value] of metaMap.entries()) {
|
|
6593
|
-
if (key === "_constraints" && value instanceof
|
|
7042
|
+
if (key === "_constraints" && value instanceof Y6.Map) {
|
|
6594
7043
|
constraints = readConstraints(value);
|
|
6595
|
-
} else if (key === "_relationships" && value instanceof
|
|
7044
|
+
} else if (key === "_relationships" && value instanceof Y6.Map) {
|
|
6596
7045
|
relationships = readRelationships(value);
|
|
6597
|
-
} else if (value instanceof
|
|
7046
|
+
} else if (value instanceof Y6.Map) {
|
|
6598
7047
|
fields[key] = readFieldMeta(value);
|
|
6599
7048
|
}
|
|
6600
7049
|
}
|
|
@@ -6613,6 +7062,10 @@ function readFieldMeta(fieldMap) {
|
|
|
6613
7062
|
if (fieldMap.get("unique") === true) field.unique = true;
|
|
6614
7063
|
if (fieldMap.get("required") === true) field.required = true;
|
|
6615
7064
|
if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
|
|
7065
|
+
const autoStamp = fieldMap.get("autoStamp");
|
|
7066
|
+
if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
|
|
7067
|
+
field.autoStamp = autoStamp;
|
|
7068
|
+
}
|
|
6616
7069
|
const def = fieldMap.get("default");
|
|
6617
7070
|
if (def !== void 0) field.default = def;
|
|
6618
7071
|
const maxLength = fieldMap.get("maxLength");
|
|
@@ -6625,7 +7078,7 @@ function inferModelFromData(dataMap) {
|
|
|
6625
7078
|
const fields = {};
|
|
6626
7079
|
let sampled = 0;
|
|
6627
7080
|
for (const [_recordId, recordValue] of dataMap.entries()) {
|
|
6628
|
-
if (!(recordValue instanceof
|
|
7081
|
+
if (!(recordValue instanceof Y6.Map)) continue;
|
|
6629
7082
|
if (++sampled > 5) break;
|
|
6630
7083
|
for (const [fieldName, value] of recordValue.entries()) {
|
|
6631
7084
|
if (fieldName.startsWith("_")) continue;
|
|
@@ -6638,7 +7091,7 @@ function inferModelFromData(dataMap) {
|
|
|
6638
7091
|
return { fields };
|
|
6639
7092
|
}
|
|
6640
7093
|
function inferTypeFromValue(value) {
|
|
6641
|
-
if (value
|
|
7094
|
+
if (isStringSetYMap(value)) return "stringset";
|
|
6642
7095
|
switch (typeof value) {
|
|
6643
7096
|
case "string":
|
|
6644
7097
|
return "string";
|
|
@@ -6653,7 +7106,7 @@ function inferTypeFromValue(value) {
|
|
|
6653
7106
|
function readConstraints(constraintsMap) {
|
|
6654
7107
|
const out = {};
|
|
6655
7108
|
for (const [name, value] of constraintsMap.entries()) {
|
|
6656
|
-
if (!(value instanceof
|
|
7109
|
+
if (!(value instanceof Y6.Map)) continue;
|
|
6657
7110
|
let fields = [];
|
|
6658
7111
|
const rawFields = value.get("fields");
|
|
6659
7112
|
if (typeof rawFields === "string") {
|
|
@@ -6672,7 +7125,7 @@ function readConstraints(constraintsMap) {
|
|
|
6672
7125
|
function readRelationships(relsMap) {
|
|
6673
7126
|
const out = {};
|
|
6674
7127
|
for (const [name, value] of relsMap.entries()) {
|
|
6675
|
-
if (!(value instanceof
|
|
7128
|
+
if (!(value instanceof Y6.Map)) continue;
|
|
6676
7129
|
const rel = {};
|
|
6677
7130
|
for (const [k, v] of value.entries()) {
|
|
6678
7131
|
rel[k] = v;
|
|
@@ -6683,6 +7136,7 @@ function readRelationships(relsMap) {
|
|
|
6683
7136
|
}
|
|
6684
7137
|
var CAMEL_TO_SNAKE = {
|
|
6685
7138
|
autoAssign: "auto_assign",
|
|
7139
|
+
autoStamp: "auto_stamp",
|
|
6686
7140
|
maxLength: "max_length",
|
|
6687
7141
|
maxCount: "max_count",
|
|
6688
7142
|
relatedIdField: "related_id_field",
|
|
@@ -6713,6 +7167,7 @@ function schemaToToml(schema) {
|
|
|
6713
7167
|
lines.push(`[models.${modelName}.fields.${fieldName}]`);
|
|
6714
7168
|
lines.push(`type = ${tomlValue(field.type)}`);
|
|
6715
7169
|
if (field.autoAssign) lines.push("auto_assign = true");
|
|
7170
|
+
if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
|
|
6716
7171
|
if (field.indexed) lines.push("indexed = true");
|
|
6717
7172
|
if (field.unique) lines.push("unique = true");
|
|
6718
7173
|
if (field.required) lines.push("required = true");
|
|
@@ -6759,10 +7214,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
|
6759
7214
|
"unique",
|
|
6760
7215
|
"required",
|
|
6761
7216
|
"auto_assign",
|
|
7217
|
+
"auto_stamp",
|
|
6762
7218
|
"max_length",
|
|
6763
7219
|
"max_count",
|
|
6764
|
-
"default"
|
|
7220
|
+
"default",
|
|
7221
|
+
"enum"
|
|
6765
7222
|
]);
|
|
7223
|
+
var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
|
|
6766
7224
|
var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
|
|
6767
7225
|
"fields",
|
|
6768
7226
|
"relationships",
|
|
@@ -6804,9 +7262,37 @@ function parseFieldOptions(raw, context, strict) {
|
|
|
6804
7262
|
if (raw.unique === true) opts.unique = true;
|
|
6805
7263
|
if (raw.required === true) opts.required = true;
|
|
6806
7264
|
if (raw.auto_assign === true) opts.autoAssign = true;
|
|
7265
|
+
if (raw.auto_stamp !== void 0) {
|
|
7266
|
+
if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
|
|
7267
|
+
throw new Error(
|
|
7268
|
+
`${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
|
|
7269
|
+
...VALID_AUTO_STAMP_VALUES
|
|
7270
|
+
].join(", ")}`
|
|
7271
|
+
);
|
|
7272
|
+
}
|
|
7273
|
+
opts.autoStamp = raw.auto_stamp;
|
|
7274
|
+
}
|
|
6807
7275
|
if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
|
|
6808
7276
|
if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
|
|
6809
7277
|
if (raw.default !== void 0) opts.default = raw.default;
|
|
7278
|
+
if (raw.enum !== void 0) {
|
|
7279
|
+
if (raw.type !== "string") {
|
|
7280
|
+
throw new Error(
|
|
7281
|
+
`${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
|
|
7282
|
+
);
|
|
7283
|
+
}
|
|
7284
|
+
if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
|
|
7285
|
+
throw new Error(
|
|
7286
|
+
`${context}: \`enum\` must be a non-empty array of strings.`
|
|
7287
|
+
);
|
|
7288
|
+
}
|
|
7289
|
+
if (!raw.enum.every((v) => typeof v === "string")) {
|
|
7290
|
+
throw new Error(
|
|
7291
|
+
`${context}: \`enum\` values must all be strings.`
|
|
7292
|
+
);
|
|
7293
|
+
}
|
|
7294
|
+
opts.enum = [...raw.enum];
|
|
7295
|
+
}
|
|
6810
7296
|
return opts;
|
|
6811
7297
|
}
|
|
6812
7298
|
function requireField(raw, field, context) {
|