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/node.d.cts
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;
|
|
@@ -495,6 +519,17 @@ declare class BaseModel implements StringSetChangeTracker {
|
|
|
495
519
|
private ensureLocalChanges;
|
|
496
520
|
private hasLocalChange;
|
|
497
521
|
private getFromYjs;
|
|
522
|
+
/**
|
|
523
|
+
* Defensive normalization for the read/sync boundary (issue #625).
|
|
524
|
+
*
|
|
525
|
+
* Stringset fields are owned by getStringSetFromYjs (which expects the
|
|
526
|
+
* raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
|
|
527
|
+
* through untouched. For any other field, a composite Yjs primitive
|
|
528
|
+
* (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
|
|
529
|
+
* raw: normalize it to its plain-JS projection (toJSON / toString) and
|
|
530
|
+
* warn, rather than throwing. Scalars pass through unchanged.
|
|
531
|
+
*/
|
|
532
|
+
private normalizeNonStringSetValue;
|
|
498
533
|
private getValue;
|
|
499
534
|
private setValue;
|
|
500
535
|
get isDirty(): boolean;
|
|
@@ -629,6 +664,27 @@ declare class BaseModel implements StringSetChangeTracker {
|
|
|
629
664
|
*/
|
|
630
665
|
static count(this: new (...args: any[]) => any, filter?: DocumentFilter, options?: Pick<QueryOptions, "documents">): Promise<number>;
|
|
631
666
|
static findAll<T extends BaseModel>(this: new (...args: any[]) => T): Promise<T[]>;
|
|
667
|
+
/**
|
|
668
|
+
* Resolve a record by its unique-constraint field values via an engine query.
|
|
669
|
+
* Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
|
|
670
|
+
* entry for a key.
|
|
671
|
+
*
|
|
672
|
+
* The index map is a cache over the SQLite projection, not the source of
|
|
673
|
+
* truth: every record is loaded into the projection on `initializeForDocument`
|
|
674
|
+
* regardless of index presence, but a record written by a client predating
|
|
675
|
+
* index maintenance (legacy data) is missing from the index. Such a record
|
|
676
|
+
* silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
|
|
677
|
+
* check. A query over the constraint fields finds it reliably.
|
|
678
|
+
*
|
|
679
|
+
* Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
|
|
680
|
+
* the document that holds it, via `queryOne`) or `null`. Callers decide
|
|
681
|
+
* whether to heal the missing index entry (write paths) and do their own
|
|
682
|
+
* logging — this method never mutates the index, so read paths stay
|
|
683
|
+
* read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
|
|
684
|
+
* constraint field means the record does not participate in the constraint,
|
|
685
|
+
* so no query runs.
|
|
686
|
+
*/
|
|
687
|
+
private static _resolveByUniqueFallback;
|
|
632
688
|
static findByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, value: any | any[]): Promise<T | null>;
|
|
633
689
|
static upsertByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, uniqueLookupValue: any | any[], dataToUpsert: Partial<Omit<InstanceType<new (...args: any[]) => T>, keyof BaseModel | "toJSON">> & {
|
|
634
690
|
id?: string;
|
|
@@ -951,6 +1007,7 @@ interface DiscoveredField {
|
|
|
951
1007
|
required?: boolean;
|
|
952
1008
|
default?: string | number | boolean;
|
|
953
1009
|
autoAssign?: boolean;
|
|
1010
|
+
autoStamp?: "create" | "update" | "both";
|
|
954
1011
|
maxLength?: number;
|
|
955
1012
|
maxCount?: number;
|
|
956
1013
|
}
|
|
@@ -1044,6 +1101,11 @@ declare function loadSchemaFromToml(filePath: string, options?: LoadSchemaOption
|
|
|
1044
1101
|
|
|
1045
1102
|
/**
|
|
1046
1103
|
* Infer a _meta_ type string from a JS runtime value.
|
|
1104
|
+
*
|
|
1105
|
+
* Only an all-`true` Y.Map (the stringset wire shape) is tagged as
|
|
1106
|
+
* `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
|
|
1107
|
+
* Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
|
|
1108
|
+
* than being mis-tagged.
|
|
1047
1109
|
*/
|
|
1048
1110
|
declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
|
|
1049
1111
|
/**
|
|
@@ -1075,6 +1137,163 @@ declare function syncModelMeta(yDoc: Y.Doc, modelName: string, schema: ModelSche
|
|
|
1075
1137
|
*/
|
|
1076
1138
|
declare function syncInferredMeta(yDoc: Y.Doc, modelName: string, recordData: Record<string, any>): void;
|
|
1077
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Write lifecycle — shared, schema-keyed write behavior for both the js-bao
|
|
1142
|
+
* client (`BaseModel.save()`) and the server-direct workflow write path
|
|
1143
|
+
* (`mutateRecord` in js-bao-wss `src/yjs-room-v2.ts`).
|
|
1144
|
+
*
|
|
1145
|
+
* Background (#1246): the server re-implemented js-bao's client write behavior,
|
|
1146
|
+
* so the two paths could drift and new js-bao field types were not picked up by
|
|
1147
|
+
* workflow writes automatically. This module is the single source of truth for
|
|
1148
|
+
* the *pure transform* (data + schema → Yjs structure), keyed only off schema
|
|
1149
|
+
* metadata — no ORM concerns (reactivity, local-change tracking, document
|
|
1150
|
+
* connection). Both paths call these helpers, so:
|
|
1151
|
+
*
|
|
1152
|
+
* 1. the stringset Yjs-shape primitives are written the same way on both sides;
|
|
1153
|
+
* 2. schema `default`s and `autoStamp`s are applied the same way on both sides;
|
|
1154
|
+
* 3. a new `FieldType` breaks `fieldWriteKind`'s exhaustive switch until it is
|
|
1155
|
+
* classified once — and the server, calling the same core, inherits it.
|
|
1156
|
+
*
|
|
1157
|
+
* The unique-index contract already lives in `../utils/uniqueIndex.ts`; this
|
|
1158
|
+
* module is the same "neither side keeps a parallel copy" pattern generalized
|
|
1159
|
+
* to the rest of the write lifecycle.
|
|
1160
|
+
*/
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Compile-time exhaustiveness guard. Passing a value whose type is not `never`
|
|
1164
|
+
* is a TypeScript error, so an unhandled `case` in an exhaustive `switch` fails
|
|
1165
|
+
* `tsc`. At runtime (reached only for a genuinely unknown value) it throws.
|
|
1166
|
+
*/
|
|
1167
|
+
declare function assertNever(x: never): never;
|
|
1168
|
+
/**
|
|
1169
|
+
* How a field's declared type is written into the record Y.Map. Every member
|
|
1170
|
+
* of the `FieldType` union must be classified here; adding a new field type to
|
|
1171
|
+
* the union without a `case` below fails `pnpm typecheck` (via `assertNever`),
|
|
1172
|
+
* which is what forces both write paths to handle it. (#1246, layer 2.)
|
|
1173
|
+
*/
|
|
1174
|
+
type FieldWriteKind = "scalar" | "stringset";
|
|
1175
|
+
declare function fieldWriteKind(type: FieldType): FieldWriteKind;
|
|
1176
|
+
/** True if `type` is a member of the `FieldType` union. */
|
|
1177
|
+
declare function isKnownFieldType(type: string): type is FieldType;
|
|
1178
|
+
/**
|
|
1179
|
+
* Resolve (or migrate to) the canonical nested `Y.Map<boolean>` for a
|
|
1180
|
+
* `stringset` field. A legacy plain-object value is carried over member by
|
|
1181
|
+
* member; the nested map is never destroyed and rebuilt, so a concurrent
|
|
1182
|
+
* client `.add()`/`.delete()` merges instead of being clobbered.
|
|
1183
|
+
*/
|
|
1184
|
+
declare function ensureStringSetYMap(rm: Y.Map<any>, fieldName: string): Y.Map<boolean>;
|
|
1185
|
+
interface StringSetDelta {
|
|
1186
|
+
add?: Iterable<string>;
|
|
1187
|
+
remove?: Iterable<string>;
|
|
1188
|
+
clear?: boolean;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Apply a per-member DELTA to a stringset target map. `clear` empties the set
|
|
1192
|
+
* first; then `remove` deletes, then `add` sets — so an `add` wins over a
|
|
1193
|
+
* `remove` of the same member within one delta. Members not named are left
|
|
1194
|
+
* untouched, so two concurrent `add`s of different members merge cleanly.
|
|
1195
|
+
*/
|
|
1196
|
+
declare function applyStringSetDelta(target: Y.Map<boolean>, delta: StringSetDelta): void;
|
|
1197
|
+
/**
|
|
1198
|
+
* REPLACE a stringset field with exactly `members`, applied as a per-member
|
|
1199
|
+
* CRDT diff (set members present, delete members no longer present) so a
|
|
1200
|
+
* concurrent client edit merges instead of being clobbered by a
|
|
1201
|
+
* destroy-and-rebuild.
|
|
1202
|
+
*/
|
|
1203
|
+
declare function replaceStringSet(rm: Y.Map<any>, fieldName: string, members: Iterable<unknown>): void;
|
|
1204
|
+
/**
|
|
1205
|
+
* Apply the client save-diff stringset change shape (`{additions, removals}`)
|
|
1206
|
+
* to the parent record's Y.Map. Thin wrapper over `applyStringSetDelta` so the
|
|
1207
|
+
* js-bao client and the server write a stringset the exact same way.
|
|
1208
|
+
*/
|
|
1209
|
+
declare function applyStringSetChange(recordYMap: Y.Map<any>, fieldName: string, change: {
|
|
1210
|
+
additions: Set<string>;
|
|
1211
|
+
removals: Set<string>;
|
|
1212
|
+
}): void;
|
|
1213
|
+
interface ParsedStringSetOp {
|
|
1214
|
+
add: string[];
|
|
1215
|
+
remove: string[];
|
|
1216
|
+
clear: boolean;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Recognize a `stringset` DELTA op-object inside a write payload:
|
|
1220
|
+
* `{ $add?: string[], $remove?: string[], $clear?: true }`. Returns the
|
|
1221
|
+
* normalized op, or `null` when `value` is not an op-object (a plain array
|
|
1222
|
+
* keeps its REPLACE meaning; a scalar is a scalar). Throws on a malformed op
|
|
1223
|
+
* (unknown `$key`, or `$add`/`$remove` not an array) so an author typo
|
|
1224
|
+
* surfaces instead of silently writing nothing. (#1242, moved here in #1246.)
|
|
1225
|
+
*/
|
|
1226
|
+
declare function parseStringSetOp(value: unknown): ParsedStringSetOp | null;
|
|
1227
|
+
/**
|
|
1228
|
+
* Write a single field's value into the record Y.Map according to the field's
|
|
1229
|
+
* declared type. Scalars are `.set()` directly; a `stringset` takes an array
|
|
1230
|
+
* (REPLACE) or a `{ $add/$remove/$clear }` op-object (DELTA). A collection
|
|
1231
|
+
* value in a non-stringset slot, or a scalar in a stringset slot, is rejected
|
|
1232
|
+
* (mirrors #1242 guardrails) rather than silently corrupting the record.
|
|
1233
|
+
*
|
|
1234
|
+
* `declaredType` is the resolved `_meta_` type (may be `undefined` when the doc
|
|
1235
|
+
* has no `_meta_` for the model — first-writer / legacy). An unresolved or
|
|
1236
|
+
* unknown type is treated as scalar for backward compatibility, but a
|
|
1237
|
+
* collection value still requires a real `stringset` type.
|
|
1238
|
+
*/
|
|
1239
|
+
declare function writeField(rm: Y.Map<any>, declaredType: string | undefined, key: string, value: unknown, modelName: string): void;
|
|
1240
|
+
declare function registerFunctionDefaultGenerator(name: string, fn: () => any): void;
|
|
1241
|
+
declare function decodeFunctionDefault(name: string): (() => any) | undefined;
|
|
1242
|
+
interface SchemaFieldLike {
|
|
1243
|
+
type?: string;
|
|
1244
|
+
default?: any;
|
|
1245
|
+
autoStamp?: "create" | "update" | "both";
|
|
1246
|
+
required?: boolean;
|
|
1247
|
+
unique?: boolean;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Resolve a field's declared default to a concrete value:
|
|
1251
|
+
* - a function → call it (client path holds real function refs);
|
|
1252
|
+
* - a `"$$literal"` string → an escaped literal; unescape the leading `$$` to
|
|
1253
|
+
* one `$` and use it verbatim (a literal default like `default: "$0.00"`,
|
|
1254
|
+
* which metaSync doubled on encode so it is not mistaken for a function ref);
|
|
1255
|
+
* - a `"$name"` string → decode via the registry and call it (server path,
|
|
1256
|
+
* where `_meta_` `$`-encodes function defaults);
|
|
1257
|
+
* - anything else → the literal value.
|
|
1258
|
+
*
|
|
1259
|
+
* An unknown `"$name"` throws (fail loud) rather than silently leaving the
|
|
1260
|
+
* field unset or writing the literal `"$..."` string into the record — a
|
|
1261
|
+
* missing server-populated field must not hide (#1246, Fork 3).
|
|
1262
|
+
*/
|
|
1263
|
+
declare function resolveDefaultValue(rawDefault: any, opts?: {
|
|
1264
|
+
fieldName?: string;
|
|
1265
|
+
modelName?: string;
|
|
1266
|
+
decode?: (name: string) => (() => any) | undefined;
|
|
1267
|
+
}): any;
|
|
1268
|
+
/**
|
|
1269
|
+
* Apply schema `default`s to a plain data object for a CREATE. A field with a
|
|
1270
|
+
* declared default that the caller did not set (absent or `undefined`) is
|
|
1271
|
+
* populated; an explicit caller value always wins. `id` is skipped — both
|
|
1272
|
+
* paths assign the id separately. Pure: mutates and returns `data`.
|
|
1273
|
+
*/
|
|
1274
|
+
declare function applyDefaultsFromSchema(fields: Record<string, SchemaFieldLike>, data: Record<string, any>, opts?: {
|
|
1275
|
+
modelName?: string;
|
|
1276
|
+
decodeFunctionDefault?: (name: string) => (() => any) | undefined;
|
|
1277
|
+
}): Record<string, any>;
|
|
1278
|
+
/**
|
|
1279
|
+
* Apply schema `autoStamp` timestamps to a plain data object. Pure transform,
|
|
1280
|
+
* with `now` injected by the caller (server passes `Date.now()`; tests pass a
|
|
1281
|
+
* fixed clock) so the result is deterministic and testable — no global clock.
|
|
1282
|
+
*
|
|
1283
|
+
* Semantics mirror the client `BaseModel.save()` stamp block:
|
|
1284
|
+
* - `'create'` → stamp only when `isNew` and the caller did not set the field
|
|
1285
|
+
* explicitly (a non-null value present in `data`);
|
|
1286
|
+
* - `'update'` / `'both'` → stamp on every save unless the caller set the
|
|
1287
|
+
* field explicitly.
|
|
1288
|
+
*
|
|
1289
|
+
* An explicit caller value (present and non-null in `data`) always wins.
|
|
1290
|
+
* Mutates and returns `data`.
|
|
1291
|
+
*/
|
|
1292
|
+
declare function applyAutoStampsFromSchema(fields: Record<string, SchemaFieldLike>, data: Record<string, any>, opts: {
|
|
1293
|
+
isNew: boolean;
|
|
1294
|
+
now: number;
|
|
1295
|
+
}): Record<string, any>;
|
|
1296
|
+
|
|
1078
1297
|
interface DumpOptions {
|
|
1079
1298
|
includeIndexes?: boolean;
|
|
1080
1299
|
}
|
|
@@ -1118,4 +1337,4 @@ declare function summarizePlainYDoc(dump: PlainYDoc): DumpSummary;
|
|
|
1118
1337
|
|
|
1119
1338
|
declare function getRecommendedNodeEngine(): Promise<string>;
|
|
1120
1339
|
|
|
1121
|
-
export { type AddRelationMethod, type AggregationAliasDebugInfo, type AggregationOperation, type AggregationOptions, type AggregationResult, BaseModel, type DatabaseConfig, DatabaseEngine, type DatabaseEngineOptions, type DatabaseEngineType, NodeDatabaseFactory as DatabaseFactory, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type FieldType, type GroupByField, type HasManyMethod, type HasManyThroughMethod, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type NodeSqliteEngineOptions, type PaginatedResult, type PaginationOptions, type PlainYDoc, RecordNotFoundError, type RefersToMethod, type RegisteredModelInfo, type RemoveRelationMethod, type SQLJSEngineOptions, type Schema, SqljsEngine, type SqljsEngineOptions, StringSet, type StringSetMembership, type UniqueConstraintConfig, UniqueConstraintViolationError, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, clearMetaSyncCache, constants, createModelClass, defineModelSchema, detectEnvironment, discoverModelNames, discoverSchema, dumpYDocToPlain, features, generateULID, getRecommendedNodeEngine, inferFieldType, initJsBao, isBrowser, isNode, isNodeModuleAvailable, loadSchemaFromToml, loadSchemaFromTomlString, registerFunctionDefault, resetJsBao, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta };
|
|
1340
|
+
export { type AddRelationMethod, type AggregationAliasDebugInfo, type AggregationOperation, type AggregationOptions, type AggregationResult, BaseModel, type DatabaseConfig, DatabaseEngine, type DatabaseEngineOptions, type DatabaseEngineType, NodeDatabaseFactory as DatabaseFactory, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type FieldType, type FieldWriteKind, type GroupByField, type HasManyMethod, type HasManyThroughMethod, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type NodeSqliteEngineOptions, type PaginatedResult, type PaginationOptions, type ParsedStringSetOp, type PlainYDoc, RecordNotFoundError, type RefersToMethod, type RegisteredModelInfo, type RemoveRelationMethod, type SQLJSEngineOptions, type Schema, type SchemaFieldLike, SqljsEngine, type SqljsEngineOptions, StringSet, type StringSetDelta, type StringSetMembership, type UniqueConstraintConfig, UniqueConstraintViolationError, applyAutoStampsFromSchema, applyDefaultsFromSchema, applyStringSetChange, applyStringSetDelta, assertNever, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, clearMetaSyncCache, constants, createModelClass, decodeFunctionDefault, defineModelSchema, detectEnvironment, discoverModelNames, discoverSchema, dumpYDocToPlain, ensureStringSetYMap, features, fieldWriteKind, generateULID, getRecommendedNodeEngine, inferFieldType, initJsBao, isBrowser, isKnownFieldType, isNode, isNodeModuleAvailable, loadSchemaFromToml, loadSchemaFromTomlString, parseStringSetOp, registerFunctionDefault, registerFunctionDefaultGenerator, replaceStringSet, resetJsBao, resolveDefaultValue, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta, writeField };
|
package/dist/node.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;
|
|
@@ -495,6 +519,17 @@ declare class BaseModel implements StringSetChangeTracker {
|
|
|
495
519
|
private ensureLocalChanges;
|
|
496
520
|
private hasLocalChange;
|
|
497
521
|
private getFromYjs;
|
|
522
|
+
/**
|
|
523
|
+
* Defensive normalization for the read/sync boundary (issue #625).
|
|
524
|
+
*
|
|
525
|
+
* Stringset fields are owned by getStringSetFromYjs (which expects the
|
|
526
|
+
* raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
|
|
527
|
+
* through untouched. For any other field, a composite Yjs primitive
|
|
528
|
+
* (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
|
|
529
|
+
* raw: normalize it to its plain-JS projection (toJSON / toString) and
|
|
530
|
+
* warn, rather than throwing. Scalars pass through unchanged.
|
|
531
|
+
*/
|
|
532
|
+
private normalizeNonStringSetValue;
|
|
498
533
|
private getValue;
|
|
499
534
|
private setValue;
|
|
500
535
|
get isDirty(): boolean;
|
|
@@ -629,6 +664,27 @@ declare class BaseModel implements StringSetChangeTracker {
|
|
|
629
664
|
*/
|
|
630
665
|
static count(this: new (...args: any[]) => any, filter?: DocumentFilter, options?: Pick<QueryOptions, "documents">): Promise<number>;
|
|
631
666
|
static findAll<T extends BaseModel>(this: new (...args: any[]) => T): Promise<T[]>;
|
|
667
|
+
/**
|
|
668
|
+
* Resolve a record by its unique-constraint field values via an engine query.
|
|
669
|
+
* Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
|
|
670
|
+
* entry for a key.
|
|
671
|
+
*
|
|
672
|
+
* The index map is a cache over the SQLite projection, not the source of
|
|
673
|
+
* truth: every record is loaded into the projection on `initializeForDocument`
|
|
674
|
+
* regardless of index presence, but a record written by a client predating
|
|
675
|
+
* index maintenance (legacy data) is missing from the index. Such a record
|
|
676
|
+
* silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
|
|
677
|
+
* check. A query over the constraint fields finds it reliably.
|
|
678
|
+
*
|
|
679
|
+
* Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
|
|
680
|
+
* the document that holds it, via `queryOne`) or `null`. Callers decide
|
|
681
|
+
* whether to heal the missing index entry (write paths) and do their own
|
|
682
|
+
* logging — this method never mutates the index, so read paths stay
|
|
683
|
+
* read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
|
|
684
|
+
* constraint field means the record does not participate in the constraint,
|
|
685
|
+
* so no query runs.
|
|
686
|
+
*/
|
|
687
|
+
private static _resolveByUniqueFallback;
|
|
632
688
|
static findByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, value: any | any[]): Promise<T | null>;
|
|
633
689
|
static upsertByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, uniqueLookupValue: any | any[], dataToUpsert: Partial<Omit<InstanceType<new (...args: any[]) => T>, keyof BaseModel | "toJSON">> & {
|
|
634
690
|
id?: string;
|
|
@@ -951,6 +1007,7 @@ interface DiscoveredField {
|
|
|
951
1007
|
required?: boolean;
|
|
952
1008
|
default?: string | number | boolean;
|
|
953
1009
|
autoAssign?: boolean;
|
|
1010
|
+
autoStamp?: "create" | "update" | "both";
|
|
954
1011
|
maxLength?: number;
|
|
955
1012
|
maxCount?: number;
|
|
956
1013
|
}
|
|
@@ -1044,6 +1101,11 @@ declare function loadSchemaFromToml(filePath: string, options?: LoadSchemaOption
|
|
|
1044
1101
|
|
|
1045
1102
|
/**
|
|
1046
1103
|
* Infer a _meta_ type string from a JS runtime value.
|
|
1104
|
+
*
|
|
1105
|
+
* Only an all-`true` Y.Map (the stringset wire shape) is tagged as
|
|
1106
|
+
* `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
|
|
1107
|
+
* Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
|
|
1108
|
+
* than being mis-tagged.
|
|
1047
1109
|
*/
|
|
1048
1110
|
declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
|
|
1049
1111
|
/**
|
|
@@ -1075,6 +1137,163 @@ declare function syncModelMeta(yDoc: Y.Doc, modelName: string, schema: ModelSche
|
|
|
1075
1137
|
*/
|
|
1076
1138
|
declare function syncInferredMeta(yDoc: Y.Doc, modelName: string, recordData: Record<string, any>): void;
|
|
1077
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Write lifecycle — shared, schema-keyed write behavior for both the js-bao
|
|
1142
|
+
* client (`BaseModel.save()`) and the server-direct workflow write path
|
|
1143
|
+
* (`mutateRecord` in js-bao-wss `src/yjs-room-v2.ts`).
|
|
1144
|
+
*
|
|
1145
|
+
* Background (#1246): the server re-implemented js-bao's client write behavior,
|
|
1146
|
+
* so the two paths could drift and new js-bao field types were not picked up by
|
|
1147
|
+
* workflow writes automatically. This module is the single source of truth for
|
|
1148
|
+
* the *pure transform* (data + schema → Yjs structure), keyed only off schema
|
|
1149
|
+
* metadata — no ORM concerns (reactivity, local-change tracking, document
|
|
1150
|
+
* connection). Both paths call these helpers, so:
|
|
1151
|
+
*
|
|
1152
|
+
* 1. the stringset Yjs-shape primitives are written the same way on both sides;
|
|
1153
|
+
* 2. schema `default`s and `autoStamp`s are applied the same way on both sides;
|
|
1154
|
+
* 3. a new `FieldType` breaks `fieldWriteKind`'s exhaustive switch until it is
|
|
1155
|
+
* classified once — and the server, calling the same core, inherits it.
|
|
1156
|
+
*
|
|
1157
|
+
* The unique-index contract already lives in `../utils/uniqueIndex.ts`; this
|
|
1158
|
+
* module is the same "neither side keeps a parallel copy" pattern generalized
|
|
1159
|
+
* to the rest of the write lifecycle.
|
|
1160
|
+
*/
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Compile-time exhaustiveness guard. Passing a value whose type is not `never`
|
|
1164
|
+
* is a TypeScript error, so an unhandled `case` in an exhaustive `switch` fails
|
|
1165
|
+
* `tsc`. At runtime (reached only for a genuinely unknown value) it throws.
|
|
1166
|
+
*/
|
|
1167
|
+
declare function assertNever(x: never): never;
|
|
1168
|
+
/**
|
|
1169
|
+
* How a field's declared type is written into the record Y.Map. Every member
|
|
1170
|
+
* of the `FieldType` union must be classified here; adding a new field type to
|
|
1171
|
+
* the union without a `case` below fails `pnpm typecheck` (via `assertNever`),
|
|
1172
|
+
* which is what forces both write paths to handle it. (#1246, layer 2.)
|
|
1173
|
+
*/
|
|
1174
|
+
type FieldWriteKind = "scalar" | "stringset";
|
|
1175
|
+
declare function fieldWriteKind(type: FieldType): FieldWriteKind;
|
|
1176
|
+
/** True if `type` is a member of the `FieldType` union. */
|
|
1177
|
+
declare function isKnownFieldType(type: string): type is FieldType;
|
|
1178
|
+
/**
|
|
1179
|
+
* Resolve (or migrate to) the canonical nested `Y.Map<boolean>` for a
|
|
1180
|
+
* `stringset` field. A legacy plain-object value is carried over member by
|
|
1181
|
+
* member; the nested map is never destroyed and rebuilt, so a concurrent
|
|
1182
|
+
* client `.add()`/`.delete()` merges instead of being clobbered.
|
|
1183
|
+
*/
|
|
1184
|
+
declare function ensureStringSetYMap(rm: Y.Map<any>, fieldName: string): Y.Map<boolean>;
|
|
1185
|
+
interface StringSetDelta {
|
|
1186
|
+
add?: Iterable<string>;
|
|
1187
|
+
remove?: Iterable<string>;
|
|
1188
|
+
clear?: boolean;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Apply a per-member DELTA to a stringset target map. `clear` empties the set
|
|
1192
|
+
* first; then `remove` deletes, then `add` sets — so an `add` wins over a
|
|
1193
|
+
* `remove` of the same member within one delta. Members not named are left
|
|
1194
|
+
* untouched, so two concurrent `add`s of different members merge cleanly.
|
|
1195
|
+
*/
|
|
1196
|
+
declare function applyStringSetDelta(target: Y.Map<boolean>, delta: StringSetDelta): void;
|
|
1197
|
+
/**
|
|
1198
|
+
* REPLACE a stringset field with exactly `members`, applied as a per-member
|
|
1199
|
+
* CRDT diff (set members present, delete members no longer present) so a
|
|
1200
|
+
* concurrent client edit merges instead of being clobbered by a
|
|
1201
|
+
* destroy-and-rebuild.
|
|
1202
|
+
*/
|
|
1203
|
+
declare function replaceStringSet(rm: Y.Map<any>, fieldName: string, members: Iterable<unknown>): void;
|
|
1204
|
+
/**
|
|
1205
|
+
* Apply the client save-diff stringset change shape (`{additions, removals}`)
|
|
1206
|
+
* to the parent record's Y.Map. Thin wrapper over `applyStringSetDelta` so the
|
|
1207
|
+
* js-bao client and the server write a stringset the exact same way.
|
|
1208
|
+
*/
|
|
1209
|
+
declare function applyStringSetChange(recordYMap: Y.Map<any>, fieldName: string, change: {
|
|
1210
|
+
additions: Set<string>;
|
|
1211
|
+
removals: Set<string>;
|
|
1212
|
+
}): void;
|
|
1213
|
+
interface ParsedStringSetOp {
|
|
1214
|
+
add: string[];
|
|
1215
|
+
remove: string[];
|
|
1216
|
+
clear: boolean;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Recognize a `stringset` DELTA op-object inside a write payload:
|
|
1220
|
+
* `{ $add?: string[], $remove?: string[], $clear?: true }`. Returns the
|
|
1221
|
+
* normalized op, or `null` when `value` is not an op-object (a plain array
|
|
1222
|
+
* keeps its REPLACE meaning; a scalar is a scalar). Throws on a malformed op
|
|
1223
|
+
* (unknown `$key`, or `$add`/`$remove` not an array) so an author typo
|
|
1224
|
+
* surfaces instead of silently writing nothing. (#1242, moved here in #1246.)
|
|
1225
|
+
*/
|
|
1226
|
+
declare function parseStringSetOp(value: unknown): ParsedStringSetOp | null;
|
|
1227
|
+
/**
|
|
1228
|
+
* Write a single field's value into the record Y.Map according to the field's
|
|
1229
|
+
* declared type. Scalars are `.set()` directly; a `stringset` takes an array
|
|
1230
|
+
* (REPLACE) or a `{ $add/$remove/$clear }` op-object (DELTA). A collection
|
|
1231
|
+
* value in a non-stringset slot, or a scalar in a stringset slot, is rejected
|
|
1232
|
+
* (mirrors #1242 guardrails) rather than silently corrupting the record.
|
|
1233
|
+
*
|
|
1234
|
+
* `declaredType` is the resolved `_meta_` type (may be `undefined` when the doc
|
|
1235
|
+
* has no `_meta_` for the model — first-writer / legacy). An unresolved or
|
|
1236
|
+
* unknown type is treated as scalar for backward compatibility, but a
|
|
1237
|
+
* collection value still requires a real `stringset` type.
|
|
1238
|
+
*/
|
|
1239
|
+
declare function writeField(rm: Y.Map<any>, declaredType: string | undefined, key: string, value: unknown, modelName: string): void;
|
|
1240
|
+
declare function registerFunctionDefaultGenerator(name: string, fn: () => any): void;
|
|
1241
|
+
declare function decodeFunctionDefault(name: string): (() => any) | undefined;
|
|
1242
|
+
interface SchemaFieldLike {
|
|
1243
|
+
type?: string;
|
|
1244
|
+
default?: any;
|
|
1245
|
+
autoStamp?: "create" | "update" | "both";
|
|
1246
|
+
required?: boolean;
|
|
1247
|
+
unique?: boolean;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Resolve a field's declared default to a concrete value:
|
|
1251
|
+
* - a function → call it (client path holds real function refs);
|
|
1252
|
+
* - a `"$$literal"` string → an escaped literal; unescape the leading `$$` to
|
|
1253
|
+
* one `$` and use it verbatim (a literal default like `default: "$0.00"`,
|
|
1254
|
+
* which metaSync doubled on encode so it is not mistaken for a function ref);
|
|
1255
|
+
* - a `"$name"` string → decode via the registry and call it (server path,
|
|
1256
|
+
* where `_meta_` `$`-encodes function defaults);
|
|
1257
|
+
* - anything else → the literal value.
|
|
1258
|
+
*
|
|
1259
|
+
* An unknown `"$name"` throws (fail loud) rather than silently leaving the
|
|
1260
|
+
* field unset or writing the literal `"$..."` string into the record — a
|
|
1261
|
+
* missing server-populated field must not hide (#1246, Fork 3).
|
|
1262
|
+
*/
|
|
1263
|
+
declare function resolveDefaultValue(rawDefault: any, opts?: {
|
|
1264
|
+
fieldName?: string;
|
|
1265
|
+
modelName?: string;
|
|
1266
|
+
decode?: (name: string) => (() => any) | undefined;
|
|
1267
|
+
}): any;
|
|
1268
|
+
/**
|
|
1269
|
+
* Apply schema `default`s to a plain data object for a CREATE. A field with a
|
|
1270
|
+
* declared default that the caller did not set (absent or `undefined`) is
|
|
1271
|
+
* populated; an explicit caller value always wins. `id` is skipped — both
|
|
1272
|
+
* paths assign the id separately. Pure: mutates and returns `data`.
|
|
1273
|
+
*/
|
|
1274
|
+
declare function applyDefaultsFromSchema(fields: Record<string, SchemaFieldLike>, data: Record<string, any>, opts?: {
|
|
1275
|
+
modelName?: string;
|
|
1276
|
+
decodeFunctionDefault?: (name: string) => (() => any) | undefined;
|
|
1277
|
+
}): Record<string, any>;
|
|
1278
|
+
/**
|
|
1279
|
+
* Apply schema `autoStamp` timestamps to a plain data object. Pure transform,
|
|
1280
|
+
* with `now` injected by the caller (server passes `Date.now()`; tests pass a
|
|
1281
|
+
* fixed clock) so the result is deterministic and testable — no global clock.
|
|
1282
|
+
*
|
|
1283
|
+
* Semantics mirror the client `BaseModel.save()` stamp block:
|
|
1284
|
+
* - `'create'` → stamp only when `isNew` and the caller did not set the field
|
|
1285
|
+
* explicitly (a non-null value present in `data`);
|
|
1286
|
+
* - `'update'` / `'both'` → stamp on every save unless the caller set the
|
|
1287
|
+
* field explicitly.
|
|
1288
|
+
*
|
|
1289
|
+
* An explicit caller value (present and non-null in `data`) always wins.
|
|
1290
|
+
* Mutates and returns `data`.
|
|
1291
|
+
*/
|
|
1292
|
+
declare function applyAutoStampsFromSchema(fields: Record<string, SchemaFieldLike>, data: Record<string, any>, opts: {
|
|
1293
|
+
isNew: boolean;
|
|
1294
|
+
now: number;
|
|
1295
|
+
}): Record<string, any>;
|
|
1296
|
+
|
|
1078
1297
|
interface DumpOptions {
|
|
1079
1298
|
includeIndexes?: boolean;
|
|
1080
1299
|
}
|
|
@@ -1118,4 +1337,4 @@ declare function summarizePlainYDoc(dump: PlainYDoc): DumpSummary;
|
|
|
1118
1337
|
|
|
1119
1338
|
declare function getRecommendedNodeEngine(): Promise<string>;
|
|
1120
1339
|
|
|
1121
|
-
export { type AddRelationMethod, type AggregationAliasDebugInfo, type AggregationOperation, type AggregationOptions, type AggregationResult, BaseModel, type DatabaseConfig, DatabaseEngine, type DatabaseEngineOptions, type DatabaseEngineType, NodeDatabaseFactory as DatabaseFactory, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type FieldType, type GroupByField, type HasManyMethod, type HasManyThroughMethod, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type NodeSqliteEngineOptions, type PaginatedResult, type PaginationOptions, type PlainYDoc, RecordNotFoundError, type RefersToMethod, type RegisteredModelInfo, type RemoveRelationMethod, type SQLJSEngineOptions, type Schema, SqljsEngine, type SqljsEngineOptions, StringSet, type StringSetMembership, type UniqueConstraintConfig, UniqueConstraintViolationError, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, clearMetaSyncCache, constants, createModelClass, defineModelSchema, detectEnvironment, discoverModelNames, discoverSchema, dumpYDocToPlain, features, generateULID, getRecommendedNodeEngine, inferFieldType, initJsBao, isBrowser, isNode, isNodeModuleAvailable, loadSchemaFromToml, loadSchemaFromTomlString, registerFunctionDefault, resetJsBao, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta };
|
|
1340
|
+
export { type AddRelationMethod, type AggregationAliasDebugInfo, type AggregationOperation, type AggregationOptions, type AggregationResult, BaseModel, type DatabaseConfig, DatabaseEngine, type DatabaseEngineOptions, type DatabaseEngineType, NodeDatabaseFactory as DatabaseFactory, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type FieldType, type FieldWriteKind, type GroupByField, type HasManyMethod, type HasManyThroughMethod, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type NodeSqliteEngineOptions, type PaginatedResult, type PaginationOptions, type ParsedStringSetOp, type PlainYDoc, RecordNotFoundError, type RefersToMethod, type RegisteredModelInfo, type RemoveRelationMethod, type SQLJSEngineOptions, type Schema, type SchemaFieldLike, SqljsEngine, type SqljsEngineOptions, StringSet, type StringSetDelta, type StringSetMembership, type UniqueConstraintConfig, UniqueConstraintViolationError, applyAutoStampsFromSchema, applyDefaultsFromSchema, applyStringSetChange, applyStringSetDelta, assertNever, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, clearMetaSyncCache, constants, createModelClass, decodeFunctionDefault, defineModelSchema, detectEnvironment, discoverModelNames, discoverSchema, dumpYDocToPlain, ensureStringSetYMap, features, fieldWriteKind, generateULID, getRecommendedNodeEngine, inferFieldType, initJsBao, isBrowser, isKnownFieldType, isNode, isNodeModuleAvailable, loadSchemaFromToml, loadSchemaFromTomlString, parseStringSetOp, registerFunctionDefault, registerFunctionDefaultGenerator, replaceStringSet, resetJsBao, resolveDefaultValue, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta, writeField };
|