@prisma-idb/family-idb 0.0.0

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/bin/prisma-idb.d.mts +1 -0
  4. package/dist/bin/prisma-idb.mjs +13 -0
  5. package/dist/bin/prisma-idb.mjs.map +1 -0
  6. package/dist/chain-order-CaHXo5jz.mjs +44 -0
  7. package/dist/chain-order-CaHXo5jz.mjs.map +1 -0
  8. package/dist/cli-Dlaym_DD.mjs +342 -0
  9. package/dist/cli-Dlaym_DD.mjs.map +1 -0
  10. package/dist/contract-space-codegen--gfuyap2.mjs +128 -0
  11. package/dist/contract-space-codegen--gfuyap2.mjs.map +1 -0
  12. package/dist/exports/cli.d.mts +26 -0
  13. package/dist/exports/cli.d.mts.map +1 -0
  14. package/dist/exports/cli.mjs +2 -0
  15. package/dist/exports/config-types.d.mts +38 -0
  16. package/dist/exports/config-types.d.mts.map +1 -0
  17. package/dist/exports/config-types.mjs +66 -0
  18. package/dist/exports/config-types.mjs.map +1 -0
  19. package/dist/exports/contract-psl.d.mts +59 -0
  20. package/dist/exports/contract-psl.d.mts.map +1 -0
  21. package/dist/exports/contract-psl.mjs +97 -0
  22. package/dist/exports/contract-psl.mjs.map +1 -0
  23. package/dist/exports/contract-ts.d.mts +98 -0
  24. package/dist/exports/contract-ts.d.mts.map +1 -0
  25. package/dist/exports/contract-ts.mjs +275 -0
  26. package/dist/exports/contract-ts.mjs.map +1 -0
  27. package/dist/exports/control.d.mts +107 -0
  28. package/dist/exports/control.d.mts.map +1 -0
  29. package/dist/exports/control.mjs +573 -0
  30. package/dist/exports/control.mjs.map +1 -0
  31. package/dist/exports/pack.d.mts +10 -0
  32. package/dist/exports/pack.d.mts.map +1 -0
  33. package/dist/exports/pack.mjs +11 -0
  34. package/dist/exports/pack.mjs.map +1 -0
  35. package/dist/migration-plan-BWhd8BdV.mjs +330 -0
  36. package/dist/migration-plan-BWhd8BdV.mjs.map +1 -0
  37. package/dist/preflight-4jDCNvS0.mjs +119 -0
  38. package/dist/preflight-4jDCNvS0.mjs.map +1 -0
  39. package/dist/psl-interpreter-C0suaXu2.d.mts +45 -0
  40. package/dist/psl-interpreter-C0suaXu2.d.mts.map +1 -0
  41. package/dist/psl-interpreter-C6Q4okuO.mjs +809 -0
  42. package/dist/psl-interpreter-C6Q4okuO.mjs.map +1 -0
  43. package/dist/validate-BLnItEKs.mjs +48 -0
  44. package/dist/validate-BLnItEKs.mjs.map +1 -0
  45. package/package.json +64 -0
@@ -0,0 +1,809 @@
1
+ import { t as validateContract } from "./validate-BLnItEKs.mjs";
2
+ import { UNBOUND_DOMAIN_NAMESPACE_ID, crossRef } from "@prisma/orm-framework/contract/types";
3
+ import { computeExecutionHash, computeProfileHash, computeStorageHash } from "@prisma/orm-framework/contract/hashing";
4
+ import { notOk, ok } from "@prisma/orm-framework/utils/result";
5
+ //#region src/core/psl-interpreter.ts
6
+ /** The one `temporal.*` type-constructor path IDB currently understands. */
7
+ const TEMPORAL_UPDATED_AT_PATH = ["temporal", "updatedAt"];
8
+ /** Generator id shared by `temporal.updatedAt()`/bare `@updatedAt`/`@default(now())`. */
9
+ const TIMESTAMP_NOW_GENERATOR_ID = "timestampNow";
10
+ /** Synthetic generator id that just echoes `params.value` — backs `@default(<literal>)`. */
11
+ const LITERAL_GENERATOR_ID = "literal";
12
+ /** Parses `@default(...)`'s positional argument as a literal value: `true`/`false`/a number/a quoted string. */
13
+ function parseDefaultLiteralValue(raw) {
14
+ const t = raw.trim();
15
+ if (t === "true") return { value: true };
16
+ if (t === "false") return { value: false };
17
+ if (/^-?\d+(\.\d+)?$/.test(t)) return { value: Number(t) };
18
+ const str = parseStringArg(t);
19
+ if (str !== void 0) return { value: str };
20
+ }
21
+ /**
22
+ * `true` if a parsed `@default(<literal>)` value's JS type matches what
23
+ * `codecId` expects (a literal's JS type must match the field's declared
24
+ * type — a string literal on an Int field, or vice versa, is a mismatch).
25
+ * Declared as a function (not a `codecId → JS type` const lookup) since
26
+ * `SCALAR_TO_CODEC_ID` — which every `codecId` here is sourced from — is
27
+ * declared further down this module; a top-level const initializer would hit
28
+ * the temporal dead zone, but a function body only evaluates when called.
29
+ */
30
+ function literalValueMatchesCodec(value, codecId) {
31
+ switch (codecId) {
32
+ case SCALAR_TO_CODEC_ID["String"]: return typeof value === "string";
33
+ case SCALAR_TO_CODEC_ID["Int"]:
34
+ case SCALAR_TO_CODEC_ID["Float"]:
35
+ case SCALAR_TO_CODEC_ID["Decimal"]: return typeof value === "number";
36
+ case SCALAR_TO_CODEC_ID["Boolean"]: return typeof value === "boolean";
37
+ default: return false;
38
+ }
39
+ }
40
+ /** Parses `@default(...)`'s positional argument as a function call: `name(...)` or bare `name()`. */
41
+ function parseDefaultFunctionCall(raw) {
42
+ const trimmed = raw.trim();
43
+ const openParen = trimmed.indexOf("(");
44
+ const closeParen = trimmed.lastIndexOf(")");
45
+ if (openParen <= 0 || closeParen !== trimmed.length - 1) return void 0;
46
+ const name = trimmed.slice(0, openParen).trim();
47
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return void 0;
48
+ const arg = trimmed.slice(openParen + 1, closeParen).trim();
49
+ return arg ? {
50
+ name,
51
+ arg
52
+ } : { name };
53
+ }
54
+ /** Resolves a parsed `@default(...)` function call (excluding `autoincrement()`, handled separately as a storage-plane concern) to a generator id. */
55
+ function resolveDefaultFunctionGeneratorId(name, arg) {
56
+ switch (name) {
57
+ case "now": return arg === void 0 ? {
58
+ ok: true,
59
+ generatorId: TIMESTAMP_NOW_GENERATOR_ID
60
+ } : {
61
+ ok: false,
62
+ reason: "invalid-argument"
63
+ };
64
+ case "uuid":
65
+ if (arg === void 0 || arg === "4") return {
66
+ ok: true,
67
+ generatorId: "uuidv4"
68
+ };
69
+ if (arg === "7") return {
70
+ ok: true,
71
+ generatorId: "uuidv7"
72
+ };
73
+ return {
74
+ ok: false,
75
+ reason: "invalid-argument"
76
+ };
77
+ case "cuid": return arg === void 0 ? {
78
+ ok: true,
79
+ generatorId: "cuid2"
80
+ } : {
81
+ ok: false,
82
+ reason: "invalid-argument"
83
+ };
84
+ default: return {
85
+ ok: false,
86
+ reason: "unknown-function"
87
+ };
88
+ }
89
+ }
90
+ const SCALAR_TO_CODEC_ID = {
91
+ String: "idb/string@1",
92
+ Int: "idb/int32@1",
93
+ Float: "idb/double@1",
94
+ Boolean: "idb/bool@1",
95
+ DateTime: "idb/date@1",
96
+ BigInt: "idb/bigint@1",
97
+ Decimal: "idb/decimal@1",
98
+ Json: "idb/json@1",
99
+ Bytes: "idb/bytes@1"
100
+ };
101
+ /**
102
+ * Codecs excluded from IndexedDB's "valid key" algorithm
103
+ * (https://w3c.github.io/IndexedDB/#key-construct: number, string, Date,
104
+ * buffer source, or Array — nothing else).
105
+ *
106
+ * - `idb/bool@1` — boolean is not, and has never been, a valid IDB key type.
107
+ * - `idb/bigint@1` — bigint round-trips fine as a stored *value* (structured
108
+ * clone supports it), but is explicitly absent from the key-type algorithm.
109
+ * - `idb/json@1` — arbitrary shape (object, array, or primitive); can't be
110
+ * statically guaranteed to be a valid key.
111
+ *
112
+ * Using any of these as a model's `@id`/key throws on every write
113
+ * (`DataError` extracting the primary key). Using one as an index `keyPath`
114
+ * doesn't throw on write — the record is just silently omitted from that
115
+ * index — but throws the first time anyone queries it via `IDBKeyRange`
116
+ * (see ADR 016's Context: `OutboxEvent.synced`, `idb/bool@1`, exactly this).
117
+ */
118
+ const IDB_INVALID_KEY_CODEC_IDS = /* @__PURE__ */ new Set([
119
+ "idb/bool@1",
120
+ "idb/bigint@1",
121
+ "idb/json@1"
122
+ ]);
123
+ /** `true` if `codecId`'s runtime representation is a valid IndexedDB key. */
124
+ function isValidIdbKeyCodec(codecId) {
125
+ return !IDB_INVALID_KEY_CODEC_IDS.has(codecId);
126
+ }
127
+ const REFERENTIAL_ACTION_MAP = {
128
+ Cascade: "cascade",
129
+ SetNull: "setNull",
130
+ SetDefault: "setDefault",
131
+ Restrict: "restrict",
132
+ NoAction: "noAction"
133
+ };
134
+ /**
135
+ * ADR 013 — shared between this file and `contract-builder.ts` (which
136
+ * already imports {@link ContractProjection} from here) so the message
137
+ * format isn't duplicated. Called whenever a surviving model's relation is
138
+ * dropped because its target is excluded — any cardinality, required or
139
+ * not; the model itself is never excluded as a result, only the relation.
140
+ */
141
+ function warnDroppedRelation(modelName, relationName, targetModel) {
142
+ console.warn(`[prisma-idb] Dropped relation "${modelName}.${relationName}" from the client contract: target model "${targetModel}" is excluded (ADR 013).`);
143
+ }
144
+ function findPositionalArg(args) {
145
+ return args.find((a) => a.kind === "positional")?.value;
146
+ }
147
+ function findNamedArg(args, name) {
148
+ return args.find((a) => a.kind === "named" && a.name === name)?.value;
149
+ }
150
+ function parseStringArg(raw) {
151
+ if (raw === void 0) return void 0;
152
+ const t = raw.trim();
153
+ if (t.startsWith("\"") && t.endsWith("\"") || t.startsWith("'") && t.endsWith("'")) return t.slice(1, -1);
154
+ }
155
+ function parseFieldList(raw) {
156
+ if (raw === void 0) return void 0;
157
+ const t = raw.trim();
158
+ if (!t.startsWith("[") || !t.endsWith("]")) return void 0;
159
+ const inner = t.slice(1, -1).trim();
160
+ if (inner === "") return [];
161
+ return inner.split(",").map((s) => s.trim()).filter(Boolean);
162
+ }
163
+ function lowerFirst(s) {
164
+ return s.charAt(0).toLowerCase() + s.slice(1);
165
+ }
166
+ function hasFieldAttribute(field, name) {
167
+ return field.attributes.some((a) => a.name === name);
168
+ }
169
+ function getFieldAttribute(field, name) {
170
+ return field.attributes.find((a) => a.name === name);
171
+ }
172
+ function hasModelAttribute(model, name) {
173
+ return model.attributes.some((a) => a.name === name);
174
+ }
175
+ const IDB_EXCLUDE_ATTR = "idb.exclude";
176
+ function interpretModel(model, modelNames, sourceId, diagnostics, projection, excludedModelNames, excludedFieldNamesByModel) {
177
+ const modelFields = Object.values(model.fields);
178
+ const excludedFieldNames = new Set(projection === "client" ? modelFields.filter((f) => hasFieldAttribute(f, IDB_EXCLUDE_ATTR)).map((f) => f.name) : []);
179
+ const storeName = parseStringArg(findPositionalArg(model.attributes.find((a) => a.name === "map")?.args ?? [])) ?? lowerFirst(model.name);
180
+ let keyPath;
181
+ let idFieldName;
182
+ const idModelAttr = model.attributes.find((a) => a.name === "id");
183
+ if (idModelAttr) {
184
+ const fields = parseFieldList(findPositionalArg(idModelAttr.args));
185
+ if (!fields || fields.length === 0) {
186
+ diagnostics.push({
187
+ code: "IDB_INVALID_ID",
188
+ message: `Model "${model.name}" @@id([…]) is missing a field list.`,
189
+ sourceId,
190
+ span: idModelAttr.span
191
+ });
192
+ return;
193
+ }
194
+ if (fields.length > 1) {
195
+ diagnostics.push({
196
+ code: "IDB_NO_COMPOUND_KEY",
197
+ message: `Model "${model.name}" @@id([${fields.join(", ")}]) declares a compound key. IDB does not support compound primary keys — use a single @id field instead.`,
198
+ sourceId,
199
+ span: idModelAttr.span
200
+ });
201
+ return;
202
+ }
203
+ idFieldName = fields[0];
204
+ keyPath = fields[0];
205
+ }
206
+ const idFields = modelFields.filter((f) => hasFieldAttribute(f, "id"));
207
+ if (idFields.length > 1) {
208
+ diagnostics.push({
209
+ code: "IDB_MULTIPLE_ID_FIELDS",
210
+ message: `Model "${model.name}" declares @id on multiple fields (${idFields.map((f) => f.name).join(", ")}). Only one @id field is allowed.`,
211
+ sourceId,
212
+ span: model.span
213
+ });
214
+ return;
215
+ }
216
+ if (idFields.length === 1) {
217
+ if (keyPath !== void 0) {
218
+ diagnostics.push({
219
+ code: "IDB_INVALID_ID",
220
+ message: `Model "${model.name}" cannot declare both a field-level @id and a model-level @@id.`,
221
+ sourceId,
222
+ span: model.span
223
+ });
224
+ return;
225
+ }
226
+ idFieldName = idFields[0].name;
227
+ keyPath = idFields[0].name;
228
+ }
229
+ if (keyPath === void 0) {
230
+ diagnostics.push({
231
+ code: "IDB_MISSING_ID",
232
+ message: `Model "${model.name}" has no @id field. Add @id to exactly one scalar field.`,
233
+ sourceId,
234
+ span: model.span
235
+ });
236
+ return;
237
+ }
238
+ if (excludedFieldNames.has(keyPath)) {
239
+ diagnostics.push({
240
+ code: "IDB_CANNOT_EXCLUDE_KEY_FIELD",
241
+ message: `Field "${model.name}.${keyPath}" is the model's @id key and cannot be marked @idb.exclude — the client contract needs a primary key for every included model.`,
242
+ sourceId,
243
+ span: model.span
244
+ });
245
+ return;
246
+ }
247
+ const indexes = {};
248
+ for (const attr of model.attributes) {
249
+ if (attr.name !== "index" && attr.name !== "unique") continue;
250
+ const isUnique = attr.name === "unique";
251
+ const fields = parseFieldList(findPositionalArg(attr.args));
252
+ if (!fields || fields.length === 0) {
253
+ diagnostics.push({
254
+ code: "IDB_INVALID_INDEX",
255
+ message: `Model "${model.name}" @@${attr.name} is missing a field list.`,
256
+ sourceId,
257
+ span: attr.span
258
+ });
259
+ continue;
260
+ }
261
+ if (fields.length > 1) {
262
+ diagnostics.push({
263
+ code: "IDB_COMPOUND_INDEX_UNSUPPORTED",
264
+ message: `Model "${model.name}" @@${attr.name}([${fields.join(", ")}]) declares a compound index. IDB compound indexes are not yet supported — use a single-field index.`,
265
+ sourceId,
266
+ span: attr.span
267
+ });
268
+ continue;
269
+ }
270
+ const field = fields[0];
271
+ if (excludedFieldNames.has(field)) {
272
+ diagnostics.push({
273
+ code: "IDB_INDEX_ON_EXCLUDED_FIELD",
274
+ message: `Model "${model.name}" @@${attr.name}([${field}]) references "${field}", which is marked @idb.exclude. Remove the index or the exclusion.`,
275
+ sourceId,
276
+ span: attr.span
277
+ });
278
+ continue;
279
+ }
280
+ const indexName = parseStringArg(findNamedArg(attr.args, "name") ?? findNamedArg(attr.args, "map")) ?? (isUnique ? `${field}_unique` : field);
281
+ indexes[indexName] = {
282
+ keyPath: field,
283
+ unique: isUnique
284
+ };
285
+ }
286
+ const contractFields = {};
287
+ const relations = {};
288
+ const relationsStorage = {};
289
+ const fieldDefaults = {};
290
+ const fksByTarget = /* @__PURE__ */ new Map();
291
+ const fieldExecutionDefaults = [];
292
+ let autoIncrement = false;
293
+ for (const field of modelFields) {
294
+ if (field.name === idFieldName && field.optional) diagnostics.push({
295
+ code: "IDB_NULLABLE_ID",
296
+ message: `Field "${model.name}.${field.name}" is marked as @id but is optional (?). The primary key cannot be nullable.`,
297
+ sourceId,
298
+ span: field.span
299
+ });
300
+ if (field.list && modelNames.has(field.typeName)) {
301
+ if (excludedModelNames.has(field.typeName)) warnDroppedRelation(model.name, field.name, field.typeName);
302
+ continue;
303
+ }
304
+ const relationAttr = getFieldAttribute(field, "relation");
305
+ if (!field.list && modelNames.has(field.typeName) && relationAttr) {
306
+ if (excludedFieldNames.has(field.name)) {
307
+ diagnostics.push({
308
+ code: "IDB_EXCLUDE_ON_RELATION_FIELD_UNSUPPORTED",
309
+ message: `Field "${model.name}.${field.name}" is a relation field and cannot be marked @idb.exclude directly. Exclude the target model with @@idb.exclude to drop the relation, or exclude a plain scalar field.`,
310
+ sourceId,
311
+ span: field.span
312
+ });
313
+ continue;
314
+ }
315
+ if (excludedModelNames.has(field.typeName)) {
316
+ warnDroppedRelation(model.name, field.name, field.typeName);
317
+ continue;
318
+ }
319
+ const args = relationAttr.args;
320
+ const localFieldsRaw = findNamedArg(args, "fields");
321
+ const targetFieldsRaw = findNamedArg(args, "references");
322
+ const localFields = parseFieldList(localFieldsRaw);
323
+ const targetFields = parseFieldList(targetFieldsRaw);
324
+ if (!localFields || localFields.length === 0 || !targetFields || targetFields.length === 0) {
325
+ diagnostics.push({
326
+ code: "IDB_INVALID_RELATION",
327
+ message: `Relation field "${model.name}.${field.name}" must declare both fields and references in @relation.`,
328
+ sourceId,
329
+ span: field.span
330
+ });
331
+ continue;
332
+ }
333
+ if (localFields.length !== targetFields.length) {
334
+ diagnostics.push({
335
+ code: "IDB_INVALID_RELATION",
336
+ message: `Relation field "${model.name}.${field.name}" must have the same number of fields and references.`,
337
+ sourceId,
338
+ span: field.span
339
+ });
340
+ continue;
341
+ }
342
+ const excludedLocalField = localFields.find((f) => excludedFieldNames.has(f));
343
+ if (excludedLocalField !== void 0) {
344
+ diagnostics.push({
345
+ code: "IDB_CANNOT_EXCLUDE_RELATION_FIELD",
346
+ message: `Field "${model.name}.${excludedLocalField}" backs relation "${model.name}.${field.name}" and cannot be excluded independently — field-level FK exclusion isn't supported (ADR 013's cascade only covers whole-model @@idb.exclude). Exclude the whole model instead, or remove the exclusion.`,
347
+ sourceId,
348
+ span: field.span
349
+ });
350
+ continue;
351
+ }
352
+ const targetExcludedFields = excludedFieldNamesByModel.get(field.typeName);
353
+ const excludedTargetField = targetExcludedFields ? targetFields.find((f) => targetExcludedFields.has(f)) : void 0;
354
+ if (excludedTargetField !== void 0) {
355
+ diagnostics.push({
356
+ code: "IDB_CANNOT_EXCLUDE_RELATION_FIELD",
357
+ message: `Field "${field.typeName}.${excludedTargetField}" is referenced by relation "${model.name}.${field.name}" and cannot be excluded independently — field-level FK exclusion isn't supported (ADR 013's cascade only covers whole-model @@idb.exclude). Exclude the whole model instead, or remove the exclusion.`,
358
+ sourceId,
359
+ span: field.span
360
+ });
361
+ continue;
362
+ }
363
+ const onDeleteRaw = findNamedArg(args, "onDelete");
364
+ const onDelete = onDeleteRaw ? REFERENTIAL_ACTION_MAP[onDeleteRaw.trim()] : void 0;
365
+ if (onDeleteRaw && onDelete === void 0) diagnostics.push({
366
+ code: "IDB_UNKNOWN_REFERENTIAL_ACTION",
367
+ message: `Relation field "${model.name}.${field.name}" has unknown onDelete value "${onDeleteRaw}". Valid values: ${Object.keys(REFERENTIAL_ACTION_MAP).join(", ")}.`,
368
+ sourceId,
369
+ span: field.span
370
+ });
371
+ const onUpdateRaw = findNamedArg(args, "onUpdate");
372
+ const onUpdate = onUpdateRaw ? REFERENTIAL_ACTION_MAP[onUpdateRaw.trim()] : void 0;
373
+ if (onUpdateRaw && onUpdate === void 0) diagnostics.push({
374
+ code: "IDB_UNKNOWN_REFERENTIAL_ACTION",
375
+ message: `Relation field "${model.name}.${field.name}" has unknown onUpdate value "${onUpdateRaw}". Valid values: ${Object.keys(REFERENTIAL_ACTION_MAP).join(", ")}.`,
376
+ sourceId,
377
+ span: field.span
378
+ });
379
+ relations[field.name] = {
380
+ to: crossRef(field.typeName),
381
+ cardinality: "N:1",
382
+ on: {
383
+ localFields,
384
+ targetFields
385
+ }
386
+ };
387
+ if (onDelete !== void 0 || onUpdate !== void 0) relationsStorage[field.name] = {
388
+ ...onDelete !== void 0 ? { onDelete } : {},
389
+ ...onUpdate !== void 0 ? { onUpdate } : {}
390
+ };
391
+ fksByTarget.set(field.typeName, {
392
+ fieldName: field.name,
393
+ localFields,
394
+ targetFields
395
+ });
396
+ for (const fkField of localFields) if (!(fkField in indexes)) indexes[fkField] = {
397
+ keyPath: fkField,
398
+ unique: false
399
+ };
400
+ continue;
401
+ }
402
+ if (!field.list && modelNames.has(field.typeName) && !relationAttr) {
403
+ diagnostics.push({
404
+ code: "IDB_MISSING_RELATION_ATTRIBUTE",
405
+ message: `Field "${model.name}.${field.name}" has model type "${field.typeName}" but no @relation attribute. Add @relation(fields: [...], references: [...]).`,
406
+ sourceId,
407
+ span: field.span
408
+ });
409
+ continue;
410
+ }
411
+ if (field.list) continue;
412
+ if (excludedFieldNames.has(field.name)) {
413
+ if (hasFieldAttribute(field, "unique")) diagnostics.push({
414
+ code: "IDB_INDEX_ON_EXCLUDED_FIELD",
415
+ message: `Field "${model.name}.${field.name}" is marked @unique but also @idb.exclude. Remove the @unique attribute or the exclusion.`,
416
+ sourceId,
417
+ span: field.span
418
+ });
419
+ continue;
420
+ }
421
+ if (field.typeConstructor) {
422
+ const path = field.typeConstructor.path;
423
+ if (!(path.length === TEMPORAL_UPDATED_AT_PATH.length && path.every((p, i) => p === TEMPORAL_UPDATED_AT_PATH[i]))) {
424
+ diagnostics.push({
425
+ code: "IDB_UNSUPPORTED_TYPE_CONSTRUCTOR",
426
+ message: `Field "${model.name}.${field.name}" uses unsupported type constructor "${path.join(".")}()". IDB currently only supports temporal.updatedAt().`,
427
+ sourceId,
428
+ span: field.typeConstructor.span
429
+ });
430
+ continue;
431
+ }
432
+ if (field.typeConstructor.args.length > 0) {
433
+ diagnostics.push({
434
+ code: "IDB_TEMPORAL_UPDATED_AT_TAKES_NO_ARGS",
435
+ message: `Field "${model.name}.${field.name}" — temporal.updatedAt() takes no arguments.`,
436
+ sourceId,
437
+ span: field.typeConstructor.span
438
+ });
439
+ continue;
440
+ }
441
+ if (field.name === idFieldName) {
442
+ diagnostics.push({
443
+ code: "IDB_TEMPORAL_UPDATED_AT_ON_KEY_FIELD",
444
+ message: `Field "${model.name}.${field.name}" is the model's @id key and cannot be temporal.updatedAt() — an auto-managed timestamp cannot also be the primary key.`,
445
+ sourceId,
446
+ span: field.span
447
+ });
448
+ continue;
449
+ }
450
+ contractFields[field.name] = {
451
+ nullable: field.optional,
452
+ type: {
453
+ kind: "scalar",
454
+ codecId: SCALAR_TO_CODEC_ID["DateTime"]
455
+ }
456
+ };
457
+ fieldExecutionDefaults.push({
458
+ fieldName: field.name,
459
+ onCreate: {
460
+ kind: "generator",
461
+ id: TIMESTAMP_NOW_GENERATOR_ID
462
+ },
463
+ onUpdate: {
464
+ kind: "generator",
465
+ id: TIMESTAMP_NOW_GENERATOR_ID
466
+ }
467
+ });
468
+ continue;
469
+ }
470
+ const codecId = SCALAR_TO_CODEC_ID[field.typeName];
471
+ if (codecId === void 0) {
472
+ diagnostics.push({
473
+ code: "IDB_UNSUPPORTED_FIELD_TYPE",
474
+ message: `Field "${model.name}.${field.name}" has unsupported type "${field.typeName}". Supported types: ${Object.keys(SCALAR_TO_CODEC_ID).join(", ")}.`,
475
+ sourceId,
476
+ span: field.span
477
+ });
478
+ continue;
479
+ }
480
+ contractFields[field.name] = {
481
+ nullable: field.optional,
482
+ type: {
483
+ kind: "scalar",
484
+ codecId
485
+ }
486
+ };
487
+ const updatedAtAttr = getFieldAttribute(field, "updatedAt");
488
+ const defaultAttr = getFieldAttribute(field, "default");
489
+ if (updatedAtAttr && defaultAttr) {
490
+ diagnostics.push({
491
+ code: "IDB_UPDATED_AT_AND_DEFAULT_CONFLICT",
492
+ message: `Field "${model.name}.${field.name}" cannot combine @updatedAt with @default(...).`,
493
+ sourceId,
494
+ span: defaultAttr.span
495
+ });
496
+ continue;
497
+ }
498
+ if (updatedAtAttr) {
499
+ if (field.name === idFieldName) {
500
+ diagnostics.push({
501
+ code: "IDB_TEMPORAL_UPDATED_AT_ON_KEY_FIELD",
502
+ message: `Field "${model.name}.${field.name}" is the model's @id key and cannot be @updatedAt — an auto-managed timestamp cannot also be the primary key.`,
503
+ sourceId,
504
+ span: field.span
505
+ });
506
+ continue;
507
+ }
508
+ if (field.optional) {
509
+ diagnostics.push({
510
+ code: "IDB_EXECUTION_DEFAULT_ON_OPTIONAL_FIELD",
511
+ message: `Field "${model.name}.${field.name}" cannot be both optional and @updatedAt — an omitted value would be ambiguous between "generate one" and "store null". Remove "?" or the attribute.`,
512
+ sourceId,
513
+ span: field.span
514
+ });
515
+ continue;
516
+ }
517
+ if (codecId !== SCALAR_TO_CODEC_ID["DateTime"]) {
518
+ diagnostics.push({
519
+ code: "IDB_UPDATED_AT_NOT_DATETIME",
520
+ message: `Field "${model.name}.${field.name}" is marked @updatedAt but has type "${field.typeName}", not DateTime.`,
521
+ sourceId,
522
+ span: field.span
523
+ });
524
+ continue;
525
+ }
526
+ fieldExecutionDefaults.push({
527
+ fieldName: field.name,
528
+ onCreate: {
529
+ kind: "generator",
530
+ id: TIMESTAMP_NOW_GENERATOR_ID
531
+ },
532
+ onUpdate: {
533
+ kind: "generator",
534
+ id: TIMESTAMP_NOW_GENERATOR_ID
535
+ }
536
+ });
537
+ } else if (defaultAttr) {
538
+ const raw = findPositionalArg(defaultAttr.args);
539
+ if (raw === void 0) {
540
+ diagnostics.push({
541
+ code: "IDB_INVALID_DEFAULT_VALUE",
542
+ message: `Field "${model.name}.${field.name}" @default(...) requires a positional argument.`,
543
+ sourceId,
544
+ span: defaultAttr.span
545
+ });
546
+ continue;
547
+ }
548
+ const literal = parseDefaultLiteralValue(raw);
549
+ if (literal) {
550
+ if (!literalValueMatchesCodec(literal.value, codecId)) {
551
+ diagnostics.push({
552
+ code: "IDB_INVALID_DEFAULT_VALUE",
553
+ message: `Field "${model.name}.${field.name}" has type "${field.typeName}" but @default(${raw}) is a ${typeof literal.value} literal.`,
554
+ sourceId,
555
+ span: defaultAttr.span
556
+ });
557
+ continue;
558
+ }
559
+ fieldExecutionDefaults.push({
560
+ fieldName: field.name,
561
+ onCreate: {
562
+ kind: "generator",
563
+ id: LITERAL_GENERATOR_ID,
564
+ params: { value: literal.value }
565
+ }
566
+ });
567
+ fieldDefaults[field.name] = literal.value;
568
+ } else {
569
+ const call = parseDefaultFunctionCall(raw);
570
+ if (!call) {
571
+ diagnostics.push({
572
+ code: "IDB_INVALID_DEFAULT_VALUE",
573
+ message: `Field "${model.name}.${field.name}" has unsupported default value "${raw}".`,
574
+ sourceId,
575
+ span: defaultAttr.span
576
+ });
577
+ continue;
578
+ }
579
+ if (call.name === "autoincrement") {
580
+ if (field.name !== idFieldName) {
581
+ diagnostics.push({
582
+ code: "IDB_AUTOINCREMENT_NOT_ON_KEY_FIELD",
583
+ message: `Field "${model.name}.${field.name}" uses @default(autoincrement()) but is not the model's @id key. IndexedDB only generates keys for the primary key field.`,
584
+ sourceId,
585
+ span: defaultAttr.span
586
+ });
587
+ continue;
588
+ }
589
+ if (codecId !== SCALAR_TO_CODEC_ID["Int"]) {
590
+ diagnostics.push({
591
+ code: "IDB_AUTOINCREMENT_NOT_INT",
592
+ message: `Field "${model.name}.${field.name}" uses @default(autoincrement()) but has type "${field.typeName}", not Int. IndexedDB key generators only produce numbers.`,
593
+ sourceId,
594
+ span: defaultAttr.span
595
+ });
596
+ continue;
597
+ }
598
+ autoIncrement = true;
599
+ continue;
600
+ }
601
+ if (field.optional) {
602
+ diagnostics.push({
603
+ code: "IDB_EXECUTION_DEFAULT_ON_OPTIONAL_FIELD",
604
+ message: `Field "${model.name}.${field.name}" cannot be both optional and @default(${call.name}(...)) — an omitted value would be ambiguous between "generate one" and "store null". Remove "?" or the default.`,
605
+ sourceId,
606
+ span: defaultAttr.span
607
+ });
608
+ continue;
609
+ }
610
+ const resolution = resolveDefaultFunctionGeneratorId(call.name, call.arg);
611
+ if (!resolution.ok) {
612
+ diagnostics.push({
613
+ code: resolution.reason === "unknown-function" ? "IDB_UNKNOWN_DEFAULT_FUNCTION" : "IDB_INVALID_DEFAULT_FUNCTION_ARGUMENT",
614
+ message: resolution.reason === "unknown-function" ? `Field "${model.name}.${field.name}" uses unsupported default function "${call.name}(...)". Supported: now(), uuid(), uuid(7), cuid(), autoincrement().` : `Field "${model.name}.${field.name}" — @default(${call.name}(${call.arg ?? ""})) has an unsupported argument.`,
615
+ sourceId,
616
+ span: defaultAttr.span
617
+ });
618
+ continue;
619
+ }
620
+ fieldExecutionDefaults.push({
621
+ fieldName: field.name,
622
+ onCreate: {
623
+ kind: "generator",
624
+ id: resolution.generatorId
625
+ }
626
+ });
627
+ }
628
+ }
629
+ if (hasFieldAttribute(field, "unique")) {
630
+ if (!Object.entries(indexes).find(([, idx]) => idx.keyPath === field.name)?.[0]) indexes[`${field.name}_unique`] = {
631
+ keyPath: field.name,
632
+ unique: true
633
+ };
634
+ }
635
+ }
636
+ const scalarCodecOf = (fieldName) => {
637
+ const type = contractFields[fieldName]?.type;
638
+ return type?.kind === "scalar" ? type.codecId : void 0;
639
+ };
640
+ const keyFieldCodec = scalarCodecOf(keyPath);
641
+ if (keyFieldCodec !== void 0 && !isValidIdbKeyCodec(keyFieldCodec)) diagnostics.push({
642
+ code: "IDB_INVALID_KEY_TYPE",
643
+ message: `Model "${model.name}" @id field "${keyPath}" has type "${keyFieldCodec}", which IndexedDB cannot use as a key. Every write would throw (DataError extracting the primary key). Use String, Int, Float, DateTime, Decimal, or Bytes instead.`,
644
+ sourceId,
645
+ span: modelFields.find((f) => f.name === keyPath)?.span ?? model.span
646
+ });
647
+ for (const [indexName, idx] of Object.entries(indexes)) {
648
+ const fieldCodec = scalarCodecOf(idx.keyPath);
649
+ if (fieldCodec === void 0 || isValidIdbKeyCodec(fieldCodec)) continue;
650
+ diagnostics.push({
651
+ code: "IDB_INVALID_INDEX_KEY_TYPE",
652
+ message: `Model "${model.name}" index "${indexName}" is keyed on "${idx.keyPath}" (type "${fieldCodec}"), which IndexedDB cannot use as an index key. Records are silently omitted from the index on write, and any query against it throws at runtime. Use String, Int, Float, DateTime, Decimal, or Bytes instead.`,
653
+ sourceId,
654
+ span: modelFields.find((f) => f.name === idx.keyPath)?.span ?? model.span
655
+ });
656
+ }
657
+ return {
658
+ modelName: model.name,
659
+ storeName,
660
+ keyPath,
661
+ indexes,
662
+ fields: contractFields,
663
+ relations,
664
+ relationsStorage,
665
+ fieldDefaults,
666
+ fksByTarget,
667
+ fieldExecutionDefaults,
668
+ autoIncrement
669
+ };
670
+ }
671
+ function interpretPslDocumentToIdbContract(table, sourceId, options) {
672
+ const projection = options?.projection ?? "full";
673
+ const diagnostics = [];
674
+ const explicitNamespaces = Object.values(table.topLevel.namespaces);
675
+ for (const ns of explicitNamespaces) diagnostics.push({
676
+ code: "IDB_UNSUPPORTED_NAMESPACE_BLOCK",
677
+ message: `IDB does not support \`namespace ${ns.name} { … }\` blocks. All models must be declared at the top level.`,
678
+ sourceId,
679
+ span: ns.span
680
+ });
681
+ const allModelsUnprojected = [...Object.values(table.topLevel.models), ...explicitNamespaces.flatMap((ns) => Object.values(ns.models))];
682
+ const modelNames = new Set(allModelsUnprojected.map((m) => m.name));
683
+ const excludedModelNames = new Set(projection === "client" ? allModelsUnprojected.filter((m) => hasModelAttribute(m, IDB_EXCLUDE_ATTR)).map((m) => m.name) : []);
684
+ const allModels = projection === "client" ? allModelsUnprojected.filter((m) => !excludedModelNames.has(m.name)) : allModelsUnprojected;
685
+ const excludedFieldNamesByModel = new Map(projection === "client" ? allModelsUnprojected.map((m) => [m.name, new Set(Object.values(m.fields).filter((f) => hasFieldAttribute(f, IDB_EXCLUDE_ATTR)).map((f) => f.name))]) : []);
686
+ const interpretedByName = /* @__PURE__ */ new Map();
687
+ for (const model of allModels) {
688
+ const result = interpretModel(model, modelNames, sourceId, diagnostics, projection, excludedModelNames, excludedFieldNamesByModel);
689
+ if (result) interpretedByName.set(model.name, result);
690
+ }
691
+ for (const model of allModels) {
692
+ const interp = interpretedByName.get(model.name);
693
+ if (!interp) continue;
694
+ for (const field of Object.values(model.fields)) {
695
+ if (!field.list || !modelNames.has(field.typeName)) continue;
696
+ if (excludedModelNames.has(field.typeName)) continue;
697
+ const targetInterp = interpretedByName.get(field.typeName);
698
+ if (!targetInterp) continue;
699
+ const fk = targetInterp.fksByTarget.get(model.name);
700
+ if (!fk) {
701
+ diagnostics.push({
702
+ code: "IDB_UNRESOLVED_BACKRELATION",
703
+ message: `Backrelation field "${model.name}.${field.name}" (list of ${field.typeName}) has no matching @relation in "${field.typeName}" pointing to "${model.name}". Add @relation(fields: [...], references: [...]) to the FK field in "${field.typeName}".`,
704
+ sourceId,
705
+ span: field.span
706
+ });
707
+ continue;
708
+ }
709
+ const mutableInterp = interp;
710
+ mutableInterp.relations[field.name] = {
711
+ to: crossRef(field.typeName),
712
+ cardinality: "1:N",
713
+ on: {
714
+ localFields: fk.targetFields,
715
+ targetFields: fk.localFields
716
+ }
717
+ };
718
+ }
719
+ }
720
+ if (diagnostics.length > 0) return notOk({
721
+ summary: "PSL to IDB contract interpretation failed",
722
+ diagnostics
723
+ });
724
+ const ns = UNBOUND_DOMAIN_NAMESPACE_ID;
725
+ const stores = {};
726
+ const roots = {};
727
+ const domainModels = {};
728
+ const executionDefaults = [];
729
+ for (const [modelName, interp] of interpretedByName) {
730
+ stores[interp.storeName] = {
731
+ keyPath: interp.keyPath,
732
+ ...interp.autoIncrement ? { autoIncrement: true } : {},
733
+ ...Object.keys(interp.indexes).length > 0 ? { indexes: interp.indexes } : {}
734
+ };
735
+ roots[interp.storeName] = crossRef(modelName);
736
+ for (const fd of interp.fieldExecutionDefaults) executionDefaults.push({
737
+ ref: {
738
+ namespace: ns,
739
+ table: interp.storeName,
740
+ column: fd.fieldName
741
+ },
742
+ ...fd.onCreate ? { onCreate: fd.onCreate } : {},
743
+ ...fd.onUpdate ? { onUpdate: fd.onUpdate } : {}
744
+ });
745
+ const modelStorage = {
746
+ storeName: interp.storeName,
747
+ keyPath: interp.keyPath,
748
+ ...Object.keys(interp.relationsStorage).length > 0 ? { relations: interp.relationsStorage } : {},
749
+ ...Object.keys(interp.fieldDefaults).length > 0 ? { fieldDefaults: interp.fieldDefaults } : {}
750
+ };
751
+ domainModels[modelName] = {
752
+ fields: interp.fields,
753
+ relations: interp.relations,
754
+ storage: modelStorage
755
+ };
756
+ }
757
+ const storageBlock = {
758
+ stores,
759
+ namespaces: { [ns]: {
760
+ id: ns,
761
+ entries: {}
762
+ } }
763
+ };
764
+ const capabilities = { idb: {
765
+ ddlOnlyInUpgrade: true,
766
+ transactionalDDL: true
767
+ } };
768
+ const storageHash = computeStorageHash({
769
+ target: "idb",
770
+ targetFamily: "idb",
771
+ storage: storageBlock
772
+ });
773
+ const profileHash = computeProfileHash({
774
+ target: "idb",
775
+ targetFamily: "idb",
776
+ capabilities
777
+ });
778
+ const storage = {
779
+ ...storageBlock,
780
+ storageHash
781
+ };
782
+ const domain = { namespaces: { [ns]: { models: domainModels } } };
783
+ const execution = executionDefaults.length > 0 ? {
784
+ executionHash: computeExecutionHash({
785
+ target: "idb",
786
+ targetFamily: "idb",
787
+ execution: { mutations: { defaults: executionDefaults } }
788
+ }),
789
+ mutations: { defaults: executionDefaults }
790
+ } : void 0;
791
+ const contract = {
792
+ target: "idb",
793
+ targetFamily: "idb",
794
+ roots,
795
+ domain,
796
+ storage,
797
+ capabilities,
798
+ extensions: {},
799
+ meta: {},
800
+ profileHash,
801
+ ...execution ? { execution } : {}
802
+ };
803
+ validateContract(contract);
804
+ return ok(contract);
805
+ }
806
+ //#endregion
807
+ export { warnDroppedRelation as i, isValidIdbKeyCodec as n, literalValueMatchesCodec as r, interpretPslDocumentToIdbContract as t };
808
+
809
+ //# sourceMappingURL=psl-interpreter-C6Q4okuO.mjs.map