js-bao 0.4.2 → 0.5.1
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/dist/browser.cjs +206 -41
- package/dist/browser.d.cts +41 -0
- package/dist/browser.d.ts +41 -0
- package/dist/browser.js +203 -39
- package/dist/client.cjs +15 -4
- package/dist/client.d.cts +78 -0
- package/dist/client.d.ts +78 -0
- package/dist/client.js +12 -2
- package/dist/cloudflare-do.cjs +191 -26
- package/dist/cloudflare-do.d.cts +127 -1
- package/dist/cloudflare-do.d.ts +127 -1
- package/dist/cloudflare-do.js +188 -24
- package/dist/cloudflare.cjs +78 -21
- package/dist/cloudflare.d.cts +84 -0
- package/dist/cloudflare.d.ts +84 -0
- package/dist/cloudflare.js +75 -19
- package/dist/codegen-v2.cjs +80 -9
- package/dist/codegen.cjs +6 -1
- package/dist/index.cjs +210 -45
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +207 -43
- package/dist/node.cjs +210 -45
- package/dist/node.d.cts +41 -0
- package/dist/node.d.ts +41 -0
- package/dist/node.js +207 -43
- package/package.json +1 -1
package/dist/cloudflare-do.d.ts
CHANGED
|
@@ -31,8 +31,32 @@ interface FieldOptions {
|
|
|
31
31
|
unique?: boolean;
|
|
32
32
|
default?: any | (() => any);
|
|
33
33
|
autoAssign?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Auto-timestamp this field with `Date.now()` (milliseconds) on save.
|
|
36
|
+
*
|
|
37
|
+
* - `'create'` — stamp only on the first save (when `isNew === true`).
|
|
38
|
+
* - `'update'` — stamp on every save (create AND update).
|
|
39
|
+
* - `'both'` — stamp on every save (create AND update).
|
|
40
|
+
*
|
|
41
|
+
* If the caller passes an explicit value for the field in `data`, the
|
|
42
|
+
* explicit value wins and the stamp is skipped. The stamp is applied
|
|
43
|
+
* in `beforeSave` BEFORE any user-defined hooks, so user hooks may
|
|
44
|
+
* still overwrite the value if they wish (last writer wins).
|
|
45
|
+
*/
|
|
46
|
+
autoStamp?: "create" | "update" | "both";
|
|
34
47
|
maxLength?: number;
|
|
35
48
|
maxCount?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Allowed-value set for a `string` field. When present, the codegen
|
|
51
|
+
* generators (database-type codegen + doc-model v2 codegen) emit a
|
|
52
|
+
* TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
|
|
53
|
+
* `string` for this field.
|
|
54
|
+
*
|
|
55
|
+
* Advisory / codegen-only: this is a TS-emission hint. The runtime and the
|
|
56
|
+
* server do NOT enforce enum membership on write (see #843). Only valid on
|
|
57
|
+
* `string` fields, and must be a non-empty array of strings.
|
|
58
|
+
*/
|
|
59
|
+
enum?: string[];
|
|
36
60
|
}
|
|
37
61
|
interface UniqueConstraintConfig {
|
|
38
62
|
name: string;
|
|
@@ -1023,6 +1047,25 @@ interface SaveRequest {
|
|
|
1023
1047
|
ifNotExists?: boolean;
|
|
1024
1048
|
condition?: DocumentFilter;
|
|
1025
1049
|
upsertOn?: string;
|
|
1050
|
+
/**
|
|
1051
|
+
* Per-call auto-timestamp directives for schemaless writes.
|
|
1052
|
+
*
|
|
1053
|
+
* Each entry is a list of field names to stamp with `Date.now()`
|
|
1054
|
+
* (milliseconds) at the corresponding lifecycle event:
|
|
1055
|
+
*
|
|
1056
|
+
* - `create` — field is stamped only when the record is new
|
|
1057
|
+
* (no row exists yet for this `modelName` + `id`).
|
|
1058
|
+
* - `update` — field is stamped on every save (create AND update).
|
|
1059
|
+
*
|
|
1060
|
+
* If the caller already provided an explicit value for the field
|
|
1061
|
+
* in `data`, the explicit value wins and the stamp is skipped.
|
|
1062
|
+
* Stamping happens BEFORE the `beforeSave` user hook runs, so user
|
|
1063
|
+
* hooks may still overwrite the stamped value if they wish.
|
|
1064
|
+
*/
|
|
1065
|
+
autoStamps?: {
|
|
1066
|
+
create?: string[];
|
|
1067
|
+
update?: string[];
|
|
1068
|
+
};
|
|
1026
1069
|
}
|
|
1027
1070
|
interface DeleteRequest {
|
|
1028
1071
|
modelName: string;
|
|
@@ -1131,6 +1174,17 @@ interface PatchRequest {
|
|
|
1131
1174
|
data: Record<string, any>;
|
|
1132
1175
|
stringSets?: Record<string, string[]>;
|
|
1133
1176
|
condition?: DocumentFilter;
|
|
1177
|
+
/**
|
|
1178
|
+
* Per-call auto-timestamp directives. A patch is always treated as
|
|
1179
|
+
* an update (`isNew === false`), so only the `update` list is
|
|
1180
|
+
* applied; the `create` list is ignored on patch.
|
|
1181
|
+
*
|
|
1182
|
+
* Explicit values in `data` win.
|
|
1183
|
+
*/
|
|
1184
|
+
autoStamps?: {
|
|
1185
|
+
create?: string[];
|
|
1186
|
+
update?: string[];
|
|
1187
|
+
};
|
|
1134
1188
|
}
|
|
1135
1189
|
interface PatchResponse {
|
|
1136
1190
|
success: boolean;
|
|
@@ -1146,6 +1200,19 @@ interface BatchOperation {
|
|
|
1146
1200
|
ifNotExists?: boolean;
|
|
1147
1201
|
condition?: DocumentFilter;
|
|
1148
1202
|
upsertOn?: string;
|
|
1203
|
+
/**
|
|
1204
|
+
* Per-operation auto-timestamp directives. Same semantics as
|
|
1205
|
+
* `SaveRequest.autoStamps` / `PatchRequest.autoStamps`. Applied
|
|
1206
|
+
* BEFORE the user `beforeSave` hook runs, with explicit values
|
|
1207
|
+
* in `data` always winning.
|
|
1208
|
+
*
|
|
1209
|
+
* For `op: "patch"`, only the `update` list applies (patches are
|
|
1210
|
+
* always treated as updates).
|
|
1211
|
+
*/
|
|
1212
|
+
autoStamps?: {
|
|
1213
|
+
create?: string[];
|
|
1214
|
+
update?: string[];
|
|
1215
|
+
};
|
|
1149
1216
|
}
|
|
1150
1217
|
interface BatchRequest {
|
|
1151
1218
|
operations: BatchOperation[];
|
|
@@ -1186,6 +1253,7 @@ interface SchemaFieldInfo {
|
|
|
1186
1253
|
required?: boolean;
|
|
1187
1254
|
default?: string | number | boolean;
|
|
1188
1255
|
autoAssign?: boolean;
|
|
1256
|
+
autoStamp?: "create" | "update" | "both";
|
|
1189
1257
|
maxLength?: number;
|
|
1190
1258
|
maxCount?: number;
|
|
1191
1259
|
}
|
|
@@ -1207,6 +1275,35 @@ interface SchemaModelInfo {
|
|
|
1207
1275
|
interface SchemaResponse {
|
|
1208
1276
|
models: Record<string, SchemaModelInfo>;
|
|
1209
1277
|
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Return an error message if `fieldName` is a reserved field name
|
|
1280
|
+
* (matches the policy enforced by `_checkReservedFields` on incoming
|
|
1281
|
+
* record data). Returns `null` if the name is acceptable.
|
|
1282
|
+
*
|
|
1283
|
+
* "id" is allowed (stripped before storage in the JSON column).
|
|
1284
|
+
* "type" maps to the internal `_type` column. Any name starting with
|
|
1285
|
+
* "_" is internal.
|
|
1286
|
+
*
|
|
1287
|
+
* @internal
|
|
1288
|
+
*/
|
|
1289
|
+
declare function checkReservedFieldName(fieldName: string): string | null;
|
|
1290
|
+
/**
|
|
1291
|
+
* Validate that none of the field names listed in `autoStamps.create` /
|
|
1292
|
+
* `autoStamps.update` are reserved. Returns an error message on the
|
|
1293
|
+
* first violation, or `null` if everything is acceptable.
|
|
1294
|
+
*
|
|
1295
|
+
* Codex P2 #2: `_checkReservedFields` only inspects the keys already
|
|
1296
|
+
* present in `data`. Without this check, a request such as
|
|
1297
|
+
* `{ autoStamps: { update: ["type"] } }` would pass the data-level
|
|
1298
|
+
* guard and then have `applyAutoStamps` add a reserved key after the
|
|
1299
|
+
* fact, bypassing the protection.
|
|
1300
|
+
*
|
|
1301
|
+
* @internal
|
|
1302
|
+
*/
|
|
1303
|
+
declare function checkAutoStampsReservedFields(autoStamps: {
|
|
1304
|
+
create?: string[];
|
|
1305
|
+
update?: string[];
|
|
1306
|
+
} | undefined): string | null;
|
|
1210
1307
|
/**
|
|
1211
1308
|
* Create a Durable Object class for schemaless database storage.
|
|
1212
1309
|
*
|
|
@@ -1432,6 +1529,7 @@ interface DiscoveredField {
|
|
|
1432
1529
|
required?: boolean;
|
|
1433
1530
|
default?: string | number | boolean;
|
|
1434
1531
|
autoAssign?: boolean;
|
|
1532
|
+
autoStamp?: "create" | "update" | "both";
|
|
1435
1533
|
maxLength?: number;
|
|
1436
1534
|
maxCount?: number;
|
|
1437
1535
|
}
|
|
@@ -1676,6 +1774,17 @@ declare class BaseModel implements StringSetChangeTracker {
|
|
|
1676
1774
|
private ensureLocalChanges;
|
|
1677
1775
|
private hasLocalChange;
|
|
1678
1776
|
private getFromYjs;
|
|
1777
|
+
/**
|
|
1778
|
+
* Defensive normalization for the read/sync boundary (issue #625).
|
|
1779
|
+
*
|
|
1780
|
+
* Stringset fields are owned by getStringSetFromYjs (which expects the
|
|
1781
|
+
* raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
|
|
1782
|
+
* through untouched. For any other field, a composite Yjs primitive
|
|
1783
|
+
* (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
|
|
1784
|
+
* raw: normalize it to its plain-JS projection (toJSON / toString) and
|
|
1785
|
+
* warn, rather than throwing. Scalars pass through unchanged.
|
|
1786
|
+
*/
|
|
1787
|
+
private normalizeNonStringSetValue;
|
|
1679
1788
|
private getValue;
|
|
1680
1789
|
private setValue;
|
|
1681
1790
|
get isDirty(): boolean;
|
|
@@ -1910,6 +2019,11 @@ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSche
|
|
|
1910
2019
|
|
|
1911
2020
|
/**
|
|
1912
2021
|
* Infer a _meta_ type string from a JS runtime value.
|
|
2022
|
+
*
|
|
2023
|
+
* Only an all-`true` Y.Map (the stringset wire shape) is tagged as
|
|
2024
|
+
* `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
|
|
2025
|
+
* Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
|
|
2026
|
+
* than being mis-tagged.
|
|
1913
2027
|
*/
|
|
1914
2028
|
declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
|
|
1915
2029
|
/**
|
|
@@ -1941,4 +2055,16 @@ declare function syncModelMeta(yDoc: Y.Doc, modelName: string, schema: ModelSche
|
|
|
1941
2055
|
*/
|
|
1942
2056
|
declare function syncInferredMeta(yDoc: Y.Doc, modelName: string, recordData: Record<string, any>): void;
|
|
1943
2057
|
|
|
1944
|
-
|
|
2058
|
+
/**
|
|
2059
|
+
* Produce a deterministic, SQL-safe alias suffix derived from arbitrary input.
|
|
2060
|
+
* The resulting string always matches our identifier policy when prefixed with
|
|
2061
|
+
* a valid identifier segment.
|
|
2062
|
+
*/
|
|
2063
|
+
declare function deterministicIdentifierHash(value: string): string;
|
|
2064
|
+
/**
|
|
2065
|
+
* Build a safe alias using a prefix that already conforms to the identifier
|
|
2066
|
+
* policy along with a deterministic hash of the raw descriptor.
|
|
2067
|
+
*/
|
|
2068
|
+
declare function buildSafeAlias(prefix: string, rawDescriptor: string): string;
|
|
2069
|
+
|
|
2070
|
+
export { type AfterQueryContext, type AfterQueryResult, type AggregateRequest, type AggregateResponse, type AggregationOperation$1 as AggregationOperation, type AggregationOptions$1 as AggregationOptions, type AggregationResult$1 as AggregationResult, type BatchOperation, type BatchOperationResult, type BatchRequest, type BatchResponse, type BeforeDeleteContext, type BeforeQueryContext, type BeforeQueryResult, type BeforeSaveContext, type CountRequest, type CountResponse, type DatabaseDOConfig, type DatabaseDOHooks, type DeleteRequest, type DeleteResponse, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, type DocumentDOConfig, type DropIndexRequest, type DropIndexResponse, type DropUniqueConstraintRequest, type DropUniqueConstraintResponse, DurableObjectEngine, type DurableObjectEngineOptions, type DurableObjectId, type DurableObjectNamespace, type DurableObjectState, type DurableObjectStorage, type DurableObjectStub, type Env, type ErrorResponse, type GroupByField$1 as GroupByField, type HookContext, type HookResult, type IncrementRequest, type IncrementResponse, type IndexEntry, type IndexListResponse, JsonQueryTranslator, type JsonQueryTranslatorOptions, JsonSchemaDDL, type JsonSchemaOptions, type PatchRequest, type PatchResponse, type QueryRequest, type QueryResponse, type RegisterIndexRequest, type RegisterIndexResponse, type RegisterUniqueConstraintRequest, type RegisterUniqueConstraintResponse, type SaveRequest, type SaveResponse, type SchemaConstraintInfo, type SchemaFieldInfo, type SchemaModelInfo, type SchemaResponse, type SqlStorage, type SqlStorageCursor, type StringSetMembership$1 as StringSetMembership, type StringSetUpdateRequest, type StringSetUpdateResponse, type SyncIndexesRequest, type SyncIndexesResponse, type UniqueConstraintEntry, type UniqueConstraintListResponse, buildSafeAlias, checkAutoStampsReservedFields, checkReservedFieldName, clearMetaSyncCache, createDatabaseDO, createDocumentDO, deterministicIdentifierHash, discoverModelNames, discoverSchema, handleRequest, inferFieldType, loadSchemaFromTomlString, registerFunctionDefault, schemaToToml, syncInferredMeta, syncModelMeta };
|
package/dist/cloudflare-do.js
CHANGED
|
@@ -17,6 +17,21 @@ function assertValidIdentifier(name, context) {
|
|
|
17
17
|
function quoteIdentifier(name) {
|
|
18
18
|
return `"${name.replace(/"/g, '""')}"`;
|
|
19
19
|
}
|
|
20
|
+
function deterministicIdentifierHash(value) {
|
|
21
|
+
let hash = 0;
|
|
22
|
+
for (let i = 0; i < value.length; i++) {
|
|
23
|
+
hash = (hash << 5) - hash + value.charCodeAt(i);
|
|
24
|
+
hash |= 0;
|
|
25
|
+
}
|
|
26
|
+
const positiveHash = hash === 0 ? 0 : Math.abs(hash);
|
|
27
|
+
return positiveHash.toString(36);
|
|
28
|
+
}
|
|
29
|
+
function buildSafeAlias(prefix, rawDescriptor) {
|
|
30
|
+
const suffix = deterministicIdentifierHash(rawDescriptor);
|
|
31
|
+
const candidate = `${prefix}_${suffix}`;
|
|
32
|
+
assertValidIdentifier(candidate, "Alias generation");
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
20
35
|
var IDENTIFIER_PATTERN;
|
|
21
36
|
var init_sql = __esm({
|
|
22
37
|
"src/utils/sql.ts"() {
|
|
@@ -312,6 +327,21 @@ var init_patterns = __esm({
|
|
|
312
327
|
}
|
|
313
328
|
});
|
|
314
329
|
|
|
330
|
+
// src/utils/yjsValues.ts
|
|
331
|
+
import * as Y from "yjs";
|
|
332
|
+
function isStringSetYMap(value) {
|
|
333
|
+
if (!(value instanceof Y.Map)) return false;
|
|
334
|
+
for (const v of value.values()) {
|
|
335
|
+
if (v !== true) return false;
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
var init_yjsValues = __esm({
|
|
340
|
+
"src/utils/yjsValues.ts"() {
|
|
341
|
+
"use strict";
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
315
345
|
// src/models/StringSet.ts
|
|
316
346
|
var init_StringSet = __esm({
|
|
317
347
|
"src/models/StringSet.ts"() {
|
|
@@ -338,9 +368,9 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
338
368
|
});
|
|
339
369
|
|
|
340
370
|
// src/models/metaSync.ts
|
|
341
|
-
import * as
|
|
371
|
+
import * as Y3 from "yjs";
|
|
342
372
|
function inferFieldType(value) {
|
|
343
|
-
if (value
|
|
373
|
+
if (isStringSetYMap(value)) return "stringset";
|
|
344
374
|
switch (typeof value) {
|
|
345
375
|
case "string":
|
|
346
376
|
return "string";
|
|
@@ -385,7 +415,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
385
415
|
if (compoundConstraints.length > 0) {
|
|
386
416
|
let constraints = meta.get("_constraints");
|
|
387
417
|
if (!constraints) {
|
|
388
|
-
constraints = new
|
|
418
|
+
constraints = new Y3.Map();
|
|
389
419
|
meta.set("_constraints", constraints);
|
|
390
420
|
}
|
|
391
421
|
for (const constraint of compoundConstraints) {
|
|
@@ -396,7 +426,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
396
426
|
if (relationships && Object.keys(relationships).length > 0) {
|
|
397
427
|
let rels = meta.get("_relationships");
|
|
398
428
|
if (!rels) {
|
|
399
|
-
rels = new
|
|
429
|
+
rels = new Y3.Map();
|
|
400
430
|
meta.set("_relationships", rels);
|
|
401
431
|
}
|
|
402
432
|
for (const [relName, relConfig] of Object.entries(relationships)) {
|
|
@@ -408,7 +438,7 @@ function syncModelMeta(yDoc, modelName, schema) {
|
|
|
408
438
|
function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
409
439
|
let fieldMeta = metaMap.get(fieldName);
|
|
410
440
|
if (!fieldMeta) {
|
|
411
|
-
fieldMeta = new
|
|
441
|
+
fieldMeta = new Y3.Map();
|
|
412
442
|
metaMap.set(fieldName, fieldMeta);
|
|
413
443
|
}
|
|
414
444
|
setIfChanged(fieldMeta, "type", fieldOpts.type);
|
|
@@ -416,6 +446,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
416
446
|
if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
|
|
417
447
|
if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
|
|
418
448
|
if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
|
|
449
|
+
if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
|
|
419
450
|
if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
|
|
420
451
|
if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
|
|
421
452
|
const encoded = encodeDefault(fieldOpts.default);
|
|
@@ -424,7 +455,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
|
|
|
424
455
|
function syncConstraintMeta(constraintsMap, constraint) {
|
|
425
456
|
let cMeta = constraintsMap.get(constraint.name);
|
|
426
457
|
if (!cMeta) {
|
|
427
|
-
cMeta = new
|
|
458
|
+
cMeta = new Y3.Map();
|
|
428
459
|
constraintsMap.set(constraint.name, cMeta);
|
|
429
460
|
}
|
|
430
461
|
setIfChanged(cMeta, "type", "unique");
|
|
@@ -434,7 +465,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
|
|
|
434
465
|
function syncRelationshipMeta(relsMap, relName, relConfig) {
|
|
435
466
|
let relMeta = relsMap.get(relName);
|
|
436
467
|
if (!relMeta) {
|
|
437
|
-
relMeta = new
|
|
468
|
+
relMeta = new Y3.Map();
|
|
438
469
|
relsMap.set(relName, relMeta);
|
|
439
470
|
}
|
|
440
471
|
for (const [key, value] of Object.entries(relConfig)) {
|
|
@@ -449,7 +480,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
|
|
|
449
480
|
if (fieldName.startsWith("_")) continue;
|
|
450
481
|
let fieldMeta = meta.get(fieldName);
|
|
451
482
|
if (!fieldMeta) {
|
|
452
|
-
fieldMeta = new
|
|
483
|
+
fieldMeta = new Y3.Map();
|
|
453
484
|
meta.set(fieldName, fieldMeta);
|
|
454
485
|
}
|
|
455
486
|
if (!fieldMeta.has("type")) {
|
|
@@ -469,13 +500,14 @@ var KNOWN_FUNCTION_DEFAULTS, _syncedCache;
|
|
|
469
500
|
var init_metaSync = __esm({
|
|
470
501
|
"src/models/metaSync.ts"() {
|
|
471
502
|
"use strict";
|
|
503
|
+
init_yjsValues();
|
|
472
504
|
KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
|
|
473
505
|
_syncedCache = /* @__PURE__ */ new WeakMap();
|
|
474
506
|
}
|
|
475
507
|
});
|
|
476
508
|
|
|
477
509
|
// src/models/BaseModel.ts
|
|
478
|
-
import * as
|
|
510
|
+
import * as Y4 from "yjs";
|
|
479
511
|
import { ulid as ulid2 } from "ulid";
|
|
480
512
|
function generateULID() {
|
|
481
513
|
return ulid2();
|
|
@@ -489,6 +521,7 @@ var init_BaseModel = __esm({
|
|
|
489
521
|
init_CursorManager();
|
|
490
522
|
init_sql();
|
|
491
523
|
init_metaSync();
|
|
524
|
+
init_yjsValues();
|
|
492
525
|
registerFunctionDefault(generateULID, "generate_ulid");
|
|
493
526
|
}
|
|
494
527
|
});
|
|
@@ -2200,6 +2233,50 @@ var JsonQueryTranslator = class {
|
|
|
2200
2233
|
init_CursorManager();
|
|
2201
2234
|
init_sql();
|
|
2202
2235
|
import { ulid } from "ulid";
|
|
2236
|
+
function checkReservedFieldName(fieldName) {
|
|
2237
|
+
if (fieldName === "id") return null;
|
|
2238
|
+
if (fieldName === "type") {
|
|
2239
|
+
return `Field 'type' is reserved (maps to internal _type column in queries)`;
|
|
2240
|
+
}
|
|
2241
|
+
if (fieldName.startsWith("_")) {
|
|
2242
|
+
return `Field '${fieldName}' is reserved (fields starting with '_' are internal)`;
|
|
2243
|
+
}
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
function checkAutoStampsReservedFields(autoStamps) {
|
|
2247
|
+
if (!autoStamps) return null;
|
|
2248
|
+
for (const field of autoStamps.create || []) {
|
|
2249
|
+
const err = checkReservedFieldName(field);
|
|
2250
|
+
if (err) return `autoStamps.create: ${err}`;
|
|
2251
|
+
}
|
|
2252
|
+
for (const field of autoStamps.update || []) {
|
|
2253
|
+
const err = checkReservedFieldName(field);
|
|
2254
|
+
if (err) return `autoStamps.update: ${err}`;
|
|
2255
|
+
}
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
function applyAutoStamps(data, autoStamps, isNew) {
|
|
2259
|
+
if (!autoStamps || !data) return;
|
|
2260
|
+
const reservedError = checkAutoStampsReservedFields(autoStamps);
|
|
2261
|
+
if (reservedError) {
|
|
2262
|
+
throw new Error(reservedError);
|
|
2263
|
+
}
|
|
2264
|
+
const now = Date.now();
|
|
2265
|
+
if (isNew && autoStamps.create) {
|
|
2266
|
+
for (const field of autoStamps.create) {
|
|
2267
|
+
if (data[field] === void 0 || data[field] === null) {
|
|
2268
|
+
data[field] = now;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
if (autoStamps.update) {
|
|
2273
|
+
for (const field of autoStamps.update) {
|
|
2274
|
+
if (data[field] === void 0 || data[field] === null) {
|
|
2275
|
+
data[field] = now;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2203
2280
|
function createDatabaseDO(config = {}) {
|
|
2204
2281
|
const hooks = config.hooks;
|
|
2205
2282
|
return class DocumentDO {
|
|
@@ -2380,7 +2457,15 @@ function createDatabaseDO(config = {}) {
|
|
|
2380
2457
|
/** @internal */
|
|
2381
2458
|
async _handleSave(request, docId) {
|
|
2382
2459
|
const body = await request.json();
|
|
2383
|
-
const {
|
|
2460
|
+
const {
|
|
2461
|
+
modelName,
|
|
2462
|
+
data,
|
|
2463
|
+
stringSets,
|
|
2464
|
+
ifNotExists,
|
|
2465
|
+
condition,
|
|
2466
|
+
upsertOn,
|
|
2467
|
+
autoStamps
|
|
2468
|
+
} = body;
|
|
2384
2469
|
let id = body.id;
|
|
2385
2470
|
if (!modelName || !id && !upsertOn) {
|
|
2386
2471
|
return this._errorResponse("modelName is required, and either id or upsertOn must be provided", 400);
|
|
@@ -2394,6 +2479,12 @@ function createDatabaseDO(config = {}) {
|
|
|
2394
2479
|
return this._errorResponse(reservedError, 400);
|
|
2395
2480
|
}
|
|
2396
2481
|
}
|
|
2482
|
+
{
|
|
2483
|
+
const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
|
|
2484
|
+
if (autoStampsReservedError) {
|
|
2485
|
+
return this._errorResponse(autoStampsReservedError, 400);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2397
2488
|
if (upsertOn) {
|
|
2398
2489
|
if (data[upsertOn] === void 0 || data[upsertOn] === null || data[upsertOn] === "") {
|
|
2399
2490
|
return this._errorResponse(
|
|
@@ -2443,8 +2534,9 @@ function createDatabaseDO(config = {}) {
|
|
|
2443
2534
|
409
|
|
2444
2535
|
);
|
|
2445
2536
|
}
|
|
2537
|
+
const isNew = !this._engine.recordExists(modelName, id);
|
|
2538
|
+
applyAutoStamps(data, autoStamps, isNew);
|
|
2446
2539
|
if (hooks?.beforeSave) {
|
|
2447
|
-
const isNew = !this._engine.recordExists(modelName, id);
|
|
2448
2540
|
const ctx = {
|
|
2449
2541
|
modelName,
|
|
2450
2542
|
docId,
|
|
@@ -2493,7 +2585,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2493
2585
|
/** @internal — Partial update: merge provided fields into existing record */
|
|
2494
2586
|
async _handlePatch(request, docId) {
|
|
2495
2587
|
const body = await request.json();
|
|
2496
|
-
const { modelName, id, data, stringSets, condition } = body;
|
|
2588
|
+
const { modelName, id, data, stringSets, condition, autoStamps } = body;
|
|
2497
2589
|
if (!modelName || !id || !data) {
|
|
2498
2590
|
return this._errorResponse("modelName, id, and data are required", 400);
|
|
2499
2591
|
}
|
|
@@ -2501,6 +2593,12 @@ function createDatabaseDO(config = {}) {
|
|
|
2501
2593
|
if (reservedError) {
|
|
2502
2594
|
return this._errorResponse(reservedError, 400);
|
|
2503
2595
|
}
|
|
2596
|
+
{
|
|
2597
|
+
const autoStampsReservedError = checkAutoStampsReservedFields(autoStamps);
|
|
2598
|
+
if (autoStampsReservedError) {
|
|
2599
|
+
return this._errorResponse(autoStampsReservedError, 400);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2504
2602
|
if (!this._engine.recordExists(modelName, id)) {
|
|
2505
2603
|
return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
|
|
2506
2604
|
}
|
|
@@ -2510,6 +2608,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2510
2608
|
409
|
|
2511
2609
|
);
|
|
2512
2610
|
}
|
|
2611
|
+
applyAutoStamps(data, autoStamps, false);
|
|
2513
2612
|
if (hooks?.beforeSave) {
|
|
2514
2613
|
const ctx = {
|
|
2515
2614
|
modelName,
|
|
@@ -2596,6 +2695,7 @@ function createDatabaseDO(config = {}) {
|
|
|
2596
2695
|
const hookDenials = /* @__PURE__ */ new Map();
|
|
2597
2696
|
const resolvedBatchIds = /* @__PURE__ */ new Map();
|
|
2598
2697
|
const opErrors = /* @__PURE__ */ new Map();
|
|
2698
|
+
const batchCreatedIds = /* @__PURE__ */ new Set();
|
|
2599
2699
|
for (let i = 0; i < operations.length; i++) {
|
|
2600
2700
|
const op = operations[i];
|
|
2601
2701
|
if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
|
|
@@ -2647,9 +2747,30 @@ function createDatabaseDO(config = {}) {
|
|
|
2647
2747
|
400
|
|
2648
2748
|
);
|
|
2649
2749
|
}
|
|
2750
|
+
const autoStampsReservedError = checkAutoStampsReservedFields(
|
|
2751
|
+
op.autoStamps
|
|
2752
|
+
);
|
|
2753
|
+
if (autoStampsReservedError) {
|
|
2754
|
+
return this._errorResponse(
|
|
2755
|
+
`Operation ${i}: ${autoStampsReservedError}`,
|
|
2756
|
+
400
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
const effectiveId = resolvedBatchIds.get(i) || op.id || "";
|
|
2760
|
+
const isNew = op.op === "save" ? (
|
|
2761
|
+
// Codex P2 #3: also treat the id as not-new if a previous
|
|
2762
|
+
// op in this batch already resolved to a save for the same
|
|
2763
|
+
// model+id. Without this, two saves to the same fresh id
|
|
2764
|
+
// both compute isNew=true against the pre-transaction
|
|
2765
|
+
// database, so a `'create'` autoStamp on the second op
|
|
2766
|
+
// would overwrite the first op's stamp.
|
|
2767
|
+
!batchCreatedIds.has(`${op.modelName}/${effectiveId}`) && !this._engine.recordExists(op.modelName, effectiveId)
|
|
2768
|
+
) : false;
|
|
2769
|
+
if (op.op === "save" && isNew && effectiveId) {
|
|
2770
|
+
batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
|
|
2771
|
+
}
|
|
2772
|
+
applyAutoStamps(op.data, op.autoStamps, isNew);
|
|
2650
2773
|
if (hooks?.beforeSave) {
|
|
2651
|
-
const effectiveId = resolvedBatchIds.get(i) || op.id || "";
|
|
2652
|
-
const isNew = op.op === "save" ? !this._engine.recordExists(op.modelName, effectiveId) : false;
|
|
2653
2774
|
const ctx = {
|
|
2654
2775
|
modelName: op.modelName,
|
|
2655
2776
|
docId,
|
|
@@ -4010,7 +4131,8 @@ async function handleRequest(request, env) {
|
|
|
4010
4131
|
}
|
|
4011
4132
|
|
|
4012
4133
|
// src/utils/yDocSchema.ts
|
|
4013
|
-
|
|
4134
|
+
init_yjsValues();
|
|
4135
|
+
import * as Y2 from "yjs";
|
|
4014
4136
|
function discoverSchema(yDoc) {
|
|
4015
4137
|
const models = {};
|
|
4016
4138
|
const metaNames = /* @__PURE__ */ new Set();
|
|
@@ -4044,7 +4166,7 @@ function discoverModelNames(yDoc) {
|
|
|
4044
4166
|
function materializeMap(yDoc, key) {
|
|
4045
4167
|
try {
|
|
4046
4168
|
const map = yDoc.getMap(key);
|
|
4047
|
-
return map instanceof
|
|
4169
|
+
return map instanceof Y2.Map ? map : null;
|
|
4048
4170
|
} catch {
|
|
4049
4171
|
return null;
|
|
4050
4172
|
}
|
|
@@ -4054,11 +4176,11 @@ function readModelMeta(metaMap) {
|
|
|
4054
4176
|
let constraints;
|
|
4055
4177
|
let relationships;
|
|
4056
4178
|
for (const [key, value] of metaMap.entries()) {
|
|
4057
|
-
if (key === "_constraints" && value instanceof
|
|
4179
|
+
if (key === "_constraints" && value instanceof Y2.Map) {
|
|
4058
4180
|
constraints = readConstraints(value);
|
|
4059
|
-
} else if (key === "_relationships" && value instanceof
|
|
4181
|
+
} else if (key === "_relationships" && value instanceof Y2.Map) {
|
|
4060
4182
|
relationships = readRelationships(value);
|
|
4061
|
-
} else if (value instanceof
|
|
4183
|
+
} else if (value instanceof Y2.Map) {
|
|
4062
4184
|
fields[key] = readFieldMeta(value);
|
|
4063
4185
|
}
|
|
4064
4186
|
}
|
|
@@ -4077,6 +4199,10 @@ function readFieldMeta(fieldMap) {
|
|
|
4077
4199
|
if (fieldMap.get("unique") === true) field.unique = true;
|
|
4078
4200
|
if (fieldMap.get("required") === true) field.required = true;
|
|
4079
4201
|
if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
|
|
4202
|
+
const autoStamp = fieldMap.get("autoStamp");
|
|
4203
|
+
if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
|
|
4204
|
+
field.autoStamp = autoStamp;
|
|
4205
|
+
}
|
|
4080
4206
|
const def = fieldMap.get("default");
|
|
4081
4207
|
if (def !== void 0) field.default = def;
|
|
4082
4208
|
const maxLength = fieldMap.get("maxLength");
|
|
@@ -4089,7 +4215,7 @@ function inferModelFromData(dataMap) {
|
|
|
4089
4215
|
const fields = {};
|
|
4090
4216
|
let sampled = 0;
|
|
4091
4217
|
for (const [_recordId, recordValue] of dataMap.entries()) {
|
|
4092
|
-
if (!(recordValue instanceof
|
|
4218
|
+
if (!(recordValue instanceof Y2.Map)) continue;
|
|
4093
4219
|
if (++sampled > 5) break;
|
|
4094
4220
|
for (const [fieldName, value] of recordValue.entries()) {
|
|
4095
4221
|
if (fieldName.startsWith("_")) continue;
|
|
@@ -4102,7 +4228,7 @@ function inferModelFromData(dataMap) {
|
|
|
4102
4228
|
return { fields };
|
|
4103
4229
|
}
|
|
4104
4230
|
function inferTypeFromValue(value) {
|
|
4105
|
-
if (value
|
|
4231
|
+
if (isStringSetYMap(value)) return "stringset";
|
|
4106
4232
|
switch (typeof value) {
|
|
4107
4233
|
case "string":
|
|
4108
4234
|
return "string";
|
|
@@ -4117,7 +4243,7 @@ function inferTypeFromValue(value) {
|
|
|
4117
4243
|
function readConstraints(constraintsMap) {
|
|
4118
4244
|
const out = {};
|
|
4119
4245
|
for (const [name, value] of constraintsMap.entries()) {
|
|
4120
|
-
if (!(value instanceof
|
|
4246
|
+
if (!(value instanceof Y2.Map)) continue;
|
|
4121
4247
|
let fields = [];
|
|
4122
4248
|
const rawFields = value.get("fields");
|
|
4123
4249
|
if (typeof rawFields === "string") {
|
|
@@ -4136,7 +4262,7 @@ function readConstraints(constraintsMap) {
|
|
|
4136
4262
|
function readRelationships(relsMap) {
|
|
4137
4263
|
const out = {};
|
|
4138
4264
|
for (const [name, value] of relsMap.entries()) {
|
|
4139
|
-
if (!(value instanceof
|
|
4265
|
+
if (!(value instanceof Y2.Map)) continue;
|
|
4140
4266
|
const rel = {};
|
|
4141
4267
|
for (const [k, v] of value.entries()) {
|
|
4142
4268
|
rel[k] = v;
|
|
@@ -4147,6 +4273,7 @@ function readRelationships(relsMap) {
|
|
|
4147
4273
|
}
|
|
4148
4274
|
var CAMEL_TO_SNAKE = {
|
|
4149
4275
|
autoAssign: "auto_assign",
|
|
4276
|
+
autoStamp: "auto_stamp",
|
|
4150
4277
|
maxLength: "max_length",
|
|
4151
4278
|
maxCount: "max_count",
|
|
4152
4279
|
relatedIdField: "related_id_field",
|
|
@@ -4177,6 +4304,7 @@ function schemaToToml(schema) {
|
|
|
4177
4304
|
lines.push(`[models.${modelName}.fields.${fieldName}]`);
|
|
4178
4305
|
lines.push(`type = ${tomlValue(field.type)}`);
|
|
4179
4306
|
if (field.autoAssign) lines.push("auto_assign = true");
|
|
4307
|
+
if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
|
|
4180
4308
|
if (field.indexed) lines.push("indexed = true");
|
|
4181
4309
|
if (field.unique) lines.push("unique = true");
|
|
4182
4310
|
if (field.required) lines.push("required = true");
|
|
@@ -4300,10 +4428,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
|
4300
4428
|
"unique",
|
|
4301
4429
|
"required",
|
|
4302
4430
|
"auto_assign",
|
|
4431
|
+
"auto_stamp",
|
|
4303
4432
|
"max_length",
|
|
4304
4433
|
"max_count",
|
|
4305
|
-
"default"
|
|
4434
|
+
"default",
|
|
4435
|
+
"enum"
|
|
4306
4436
|
]);
|
|
4437
|
+
var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
|
|
4307
4438
|
var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
|
|
4308
4439
|
"fields",
|
|
4309
4440
|
"relationships",
|
|
@@ -4345,9 +4476,37 @@ function parseFieldOptions(raw, context, strict) {
|
|
|
4345
4476
|
if (raw.unique === true) opts.unique = true;
|
|
4346
4477
|
if (raw.required === true) opts.required = true;
|
|
4347
4478
|
if (raw.auto_assign === true) opts.autoAssign = true;
|
|
4479
|
+
if (raw.auto_stamp !== void 0) {
|
|
4480
|
+
if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
|
|
4481
|
+
throw new Error(
|
|
4482
|
+
`${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
|
|
4483
|
+
...VALID_AUTO_STAMP_VALUES
|
|
4484
|
+
].join(", ")}`
|
|
4485
|
+
);
|
|
4486
|
+
}
|
|
4487
|
+
opts.autoStamp = raw.auto_stamp;
|
|
4488
|
+
}
|
|
4348
4489
|
if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
|
|
4349
4490
|
if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
|
|
4350
4491
|
if (raw.default !== void 0) opts.default = raw.default;
|
|
4492
|
+
if (raw.enum !== void 0) {
|
|
4493
|
+
if (raw.type !== "string") {
|
|
4494
|
+
throw new Error(
|
|
4495
|
+
`${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
|
|
4496
|
+
);
|
|
4497
|
+
}
|
|
4498
|
+
if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
|
|
4499
|
+
throw new Error(
|
|
4500
|
+
`${context}: \`enum\` must be a non-empty array of strings.`
|
|
4501
|
+
);
|
|
4502
|
+
}
|
|
4503
|
+
if (!raw.enum.every((v) => typeof v === "string")) {
|
|
4504
|
+
throw new Error(
|
|
4505
|
+
`${context}: \`enum\` values must all be strings.`
|
|
4506
|
+
);
|
|
4507
|
+
}
|
|
4508
|
+
opts.enum = [...raw.enum];
|
|
4509
|
+
}
|
|
4351
4510
|
return opts;
|
|
4352
4511
|
}
|
|
4353
4512
|
function requireField(raw, field, context) {
|
|
@@ -4471,13 +4630,18 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
4471
4630
|
|
|
4472
4631
|
// src/cloudflare-do.ts
|
|
4473
4632
|
init_metaSync();
|
|
4633
|
+
init_sql();
|
|
4474
4634
|
export {
|
|
4475
4635
|
DurableObjectEngine,
|
|
4476
4636
|
JsonQueryTranslator,
|
|
4477
4637
|
JsonSchemaDDL,
|
|
4638
|
+
buildSafeAlias,
|
|
4639
|
+
checkAutoStampsReservedFields,
|
|
4640
|
+
checkReservedFieldName,
|
|
4478
4641
|
clearMetaSyncCache,
|
|
4479
4642
|
createDatabaseDO,
|
|
4480
4643
|
createDocumentDO,
|
|
4644
|
+
deterministicIdentifierHash,
|
|
4481
4645
|
discoverModelNames,
|
|
4482
4646
|
discoverSchema,
|
|
4483
4647
|
handleRequest,
|