@prisma-idb/sync-server 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -17,9 +17,23 @@ const syncServer = createSyncServer({
17
17
  getKeyField: sqlGetKeyField, // for a SQL contract
18
18
  });
19
19
 
20
+ // payloads use native application values (Date, bigint, Uint8Array).
20
21
  const checks = syncServer.validatePush(events, { scopeKey: signedInUserId });
22
+ for (const { check } of checks) {
23
+ if (check.kind === "validation-failure") {
24
+ // Reject with check.error: RECORD_VALIDATION_FAILURE or KEYPATH_VALIDATION_FAILURE.
25
+ continue;
26
+ }
27
+ // Execute the ownership check using your database.
28
+ }
21
29
  ```
22
30
 
31
+ Records validate against the client projection and reject extra fields and undeclared enum values. Creates require all client-visible non-nullable fields; updates validate supplied fields, and deletes validate only keys. Validation failures occur before ownership paths are resolved. The server builds validators for every client-visible model at construction and throws with the model and codec ID if a validator is unavailable. Server-only models and fields are excluded from this check. Key validation and ownership checks use the full server contract. The server must fill required server-only fields before insertion; omitting them can still fail at the database boundary.
32
+
33
+ The default SQL lookup validates Date codecs (including legacy `pg/date@1`, `pg/timestamp@1`, `pg/timestamptz@1` and `sql/timestamp@1`), string date/time codecs, primitive scalars, JSON, bytes and text arrays against their application values. `pg/*-temporal@1` values are Temporal objects, and `pg/interval@1` values are `{ months, days, micros }` objects; neither has a default validator. Choose Date/string representations for synced date/time fields. Historical codec validation does not restore codecs removed from the installed SQL runtime: re-emit old contracts before using them with rc.12.
34
+
35
+ Other families can extend the exported `defaultValidationCodecs` with a `codecLookup` that returns supported application names: `string`, `string[]`, `number`, `boolean`, `bigint`, `Date`, `Uint8Array` or `unknown`. Names such as `Temporal.Instant` are rejected at construction. `unknown` deliberately skips scalar type checking and requires the caller to validate that codec's value before pushing; it is not a Temporal or interval validator.
36
+
23
37
  For a SQL database, [`@prisma-idb/sync-server-sql`](https://www.npmjs.com/package/@prisma-idb/sync-server-sql) runs the checks and writes for you.
24
38
 
25
39
  ## Entry points
package/dist/postgres.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as sqlContractWithSync } from "./sql-contract-0ZmfUgDg.mjs";
1
+ import { t as sqlContractWithSync } from "./sql-contract-DVUxPtOh.mjs";
2
2
  import { defineConfig as defineConfig$1 } from "@prisma/orm-framework/config/config-types";
3
3
  import { ifDefined } from "@prisma/orm-framework/utils/defined";
4
4
  import postgresAdapter from "@prisma/orm-postgres/adapter/control";
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.mts","names":[],"sources":["../src/core/changelog-schema.ts","../src/core/sql-contract.ts"],"mappings":";;;;iBA4DgB,wBAAwB;;;;;;;;;;iBAaxB,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCtCzB,oBAAoB,oBAAoB,SAAS,wBAAwB"}
1
+ {"version":3,"file":"schema.d.mts","names":[],"sources":["../src/core/changelog-schema.ts","../src/core/sql-contract.ts"],"mappings":";;;;iBAgEgB,wBAAwB;;;;;;;;;;iBAaxB,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBC1CzB,oBAAoB,oBAAoB,SAAS,wBAAwB"}
package/dist/schema.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as injectChangelogModelSql, r as prepareSqlSchemaWithSync, t as sqlContractWithSync } from "./sql-contract-0ZmfUgDg.mjs";
1
+ import { n as injectChangelogModelSql, r as prepareSqlSchemaWithSync, t as sqlContractWithSync } from "./sql-contract-DVUxPtOh.mjs";
2
2
  export { injectChangelogModelSql, prepareSqlSchemaWithSync, sqlContractWithSync };
package/dist/server.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Contract } from "@prisma/orm-framework/contract/types";
2
+ import { ValidationCodecLookup } from "@prisma-idb/target-idb/runtime";
2
3
  //#region src/core/ownership-dag.d.ts
3
4
  /**
4
5
  * The contract shape `sync-server` operates on — any family's `Contract`
@@ -56,7 +57,10 @@ interface SyncPushEvent {
56
57
  readonly id: string;
57
58
  readonly model: string;
58
59
  readonly operation: "create" | "update" | "delete";
60
+ /** Native application values used for contract validation. */
59
61
  readonly payload: Record<string, unknown>;
62
+ /** Pre-revival payload; supply when native keys differ from their JSON wire form. */
63
+ readonly wirePayload?: Record<string, unknown>;
60
64
  }
61
65
  /**
62
66
  * A changelog row pending pull; `key` is the row's own primary-key value.
@@ -101,10 +105,15 @@ type OwnershipCheck = {
101
105
  */
102
106
  readonly paths: readonly (readonly string[])[];
103
107
  };
108
+ type PushCheck = OwnershipCheck | {
109
+ readonly kind: "validation-failure";
110
+ readonly error: "RECORD_VALIDATION_FAILURE" | "KEYPATH_VALIDATION_FAILURE";
111
+ readonly issues: readonly string[];
112
+ };
104
113
  interface PushValidationResult {
105
114
  readonly eventId: string;
106
115
  readonly model: string;
107
- readonly check: OwnershipCheck;
116
+ readonly check: PushCheck;
108
117
  }
109
118
  interface PullScopeResult {
110
119
  readonly changelogId: string;
@@ -130,8 +139,9 @@ type GetKeyField = (contract: SyncServerContract, modelName: string) => string;
130
139
  * `getKeyField` instead of silently misresolving.
131
140
  */
132
141
  declare const defaultGetKeyField: GetKeyField;
133
- declare function validatePush(dag: OwnershipDag, contract: SyncServerContract, getKeyField: GetKeyField, events: readonly SyncPushEvent[], options: {
142
+ declare function validatePush(dag: OwnershipDag, contract: SyncServerContract, clientContract: SyncServerContract, getKeyField: GetKeyField, events: readonly SyncPushEvent[], options: {
134
143
  readonly scopeKey: string;
144
+ readonly codecLookup?: ValidationCodecLookup;
135
145
  }): readonly PushValidationResult[];
136
146
  declare function buildPullQueries(dag: OwnershipDag, contract: SyncServerContract, getKeyField: GetKeyField, logs: readonly SyncPullLogEntry[], options: {
137
147
  readonly scopeKey: string;
@@ -139,11 +149,13 @@ declare function buildPullQueries(dag: OwnershipDag, contract: SyncServerContrac
139
149
  interface CreateSyncServerOptions {
140
150
  /** The full server-side contract (ADR 012) — includes client-excluded models. Any family. */
141
151
  readonly contract: SyncServerContract;
142
- /** The client-projected contract (ADR 012) — defines which models are ever synced. Any family. */
152
+ /** The client-projected contract (ADR 012) — defines the synced models and fields to validate. Any family. */
143
153
  readonly clientContract: SyncServerContract;
144
154
  readonly rootModel: string;
145
155
  /** @default defaultGetKeyField (IDB-shaped storage.keyPath) */
146
156
  readonly getKeyField?: GetKeyField;
157
+ /** Native application types; unsupported validators throw at construction. */
158
+ readonly codecLookup?: ValidationCodecLookup;
147
159
  }
148
160
  interface SyncServer {
149
161
  readonly rootModel: string;
@@ -162,5 +174,8 @@ interface SyncServer {
162
174
  */
163
175
  declare function createSyncServer(options: CreateSyncServerOptions): SyncServer;
164
176
  //#endregion
165
- export { type CreateSyncServerOptions, type GetKeyField, type OwnershipCheck, type OwnershipDag, type PullScopeResult, type PushValidationResult, type SyncPullLogEntry, type SyncPushEvent, type SyncServer, type SyncServerContract, buildOwnershipDag, buildPullQueries, createSyncServer, defaultGetKeyField, resolveAuthorizationPaths, validatePush };
177
+ //#region src/core/validation-codecs.d.ts
178
+ declare const defaultValidationCodecs: ValidationCodecLookup;
179
+ //#endregion
180
+ export { type CreateSyncServerOptions, type GetKeyField, type OwnershipCheck, type OwnershipDag, type PullScopeResult, type PushCheck, type PushValidationResult, type SyncPullLogEntry, type SyncPushEvent, type SyncServer, type SyncServerContract, buildOwnershipDag, buildPullQueries, createSyncServer, defaultGetKeyField, defaultValidationCodecs, resolveAuthorizationPaths, validatePush };
166
181
  //# sourceMappingURL=server.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../src/core/ownership-dag.ts","../src/core/authorization-paths.ts","../src/core/sync-server.ts"],"mappings":";;;;;;;;;;;KAYY,qBAAqB;;;;;;;UAQhB;WACN;WACA,OAAO,oBAAoB;;WAE3B,cAAc;;;;;;;;;;;;;;iBAeT,kBACd,UAAU,oBACV,gBAAgB,oBAChB,oBACC;;;;;;;;;;;;;;iBC7Ba,0BACd,UAAU,oBACV,mBACA;;;;UCXe;WACN;WACA;WACA;WACA,SAAS;;;;;;;;;;;;;UAcH;WACN;WACA;WACA;;;;;;;KAQC;WACG;;WAEA;WACA;WACA;WACA;;WAEA;;WAGA;WACA;WACA;WACA;WACA;;;;;;WAMA;;UAGE;WACN;WACA;WACA,OAAO;;UAGD;WACN;WACA;WACA,OAAO;;;;;;;;;;;KAYN,eAAe,UAAU,oBAAoB;;;;;;;;;cAU5C,oBAAoB;iBAmCjB,aACd,KAAK,cACL,UAAU,oBACV,aAAa,aACb,iBAAiB,iBACjB;WAAoB;aACV;iBAqBI,iBACd,KAAK,cACL,UAAU,oBACV,aAAa,aACb,eAAe,oBACf;WAAoB;aACV;UAaK;;WAEN,UAAU;;WAEV,gBAAgB;WAChB;;WAEA,cAAc;;UAGR;WACN;;EAET,aACE,iBAAiB,iBACjB;aAAoB;eACV;;EAEZ,iBACE,eAAe,oBACf;aAAoB;eACV;;;;;;iBAOE,iBAAiB,SAAS,0BAA0B"}
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../src/core/ownership-dag.ts","../src/core/authorization-paths.ts","../src/core/sync-server.ts","../src/core/validation-codecs.ts"],"mappings":";;;;;;;;;;;;KAYY,qBAAqB;;;;;;;UAQhB;WACN;WACA,OAAO,oBAAoB;;WAE3B,cAAc;;;;;;;;;;;;;;iBAeT,kBACd,UAAU,oBACV,gBAAgB,oBAChB,oBACC;;;;;;;;;;;;;;iBC7Ba,0BACd,UAAU,oBACV,mBACA;;;;UCRe;WACN;WACA;WACA;;WAEA,SAAS;;WAET,cAAc;;;;;;;;;;;;;UAcR;WACN;WACA;WACA;;;;;;;KAQC;WACG;;WAEA;WACA;WACA;WACA;;WAEA;;WAGA;WACA;WACA;WACA;WACA;;;;;;WAMA;;KAGH,YACR;WAEW;WACA;WACA;;UAGE;WACN;WACA;WACA,OAAO;;UAGD;WACN;WACA;WACA,OAAO;;;;;;;;;;;KAYN,eAAe,UAAU,oBAAoB;;;;;;;;;cAU5C,oBAAoB;iBAmCjB,aACd,KAAK,cACL,UAAU,oBACV,gBAAgB,oBAChB,aAAa,aACb,iBAAiB,iBACjB;WAAoB;WAA2B,cAAc;aACnD;iBA0DI,iBACd,KAAK,cACL,UAAU,oBACV,aAAa,aACb,eAAe,oBACf;WAAoB;aACV;UAaK;;WAEN,UAAU;;WAEV,gBAAgB;WAChB;;WAEA,cAAc;;WAEd,cAAc;;UAGR;WACN;;EAET,aACE,iBAAiB,iBACjB;aAAoB;eACV;;EAEZ,iBACE,eAAe,oBACf;aAAoB;eACV;;;;;;iBAOE,iBAAiB,SAAS,0BAA0B;;;cC/MvD,yBAAyB"}
package/dist/server.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
2
+ import { assertRecordValidator, idbCodecLookup, validateKeyFields, validateRecord } from "@prisma-idb/target-idb/runtime";
2
3
  //#region src/core/ownership-dag.ts
3
4
  /**
4
5
  * Builds the ownership DAG from the full server-side contract, walking
@@ -93,6 +94,56 @@ function resolveAuthorizationPaths(contract, rootModel, modelName) {
93
94
  return paths.sort((a, b) => a.length - b.length || a.join(".").localeCompare(b.join(".")));
94
95
  }
95
96
  //#endregion
97
+ //#region src/core/validation-codecs.ts
98
+ const sqlApplicationTypes = {};
99
+ for (const name of [
100
+ "text",
101
+ "char",
102
+ "varchar"
103
+ ]) sqlApplicationTypes[`sql/${name}@1`] = ["string"];
104
+ for (const name of ["int", "float"]) sqlApplicationTypes[`sql/${name}@1`] = ["number"];
105
+ for (const name of [
106
+ "text",
107
+ "enum",
108
+ "char",
109
+ "varchar",
110
+ "uuid",
111
+ "numeric",
112
+ "inet",
113
+ "bit",
114
+ "varbit",
115
+ "time",
116
+ "timetz",
117
+ "date-string",
118
+ "timestamp-string",
119
+ "timestamptz-string",
120
+ "time-string",
121
+ "tsquery"
122
+ ]) sqlApplicationTypes[`pg/${name}@1`] = ["string"];
123
+ for (const name of [
124
+ "int",
125
+ "int2",
126
+ "int4",
127
+ "int8number",
128
+ "float",
129
+ "float4",
130
+ "float8"
131
+ ]) sqlApplicationTypes[`pg/${name}@1`] = ["number"];
132
+ for (const name of ["int8", "unboundedint"]) sqlApplicationTypes[`pg/${name}@1`] = ["bigint"];
133
+ sqlApplicationTypes["pg/bool@1"] = ["boolean"];
134
+ for (const name of [
135
+ "date",
136
+ "timestamp",
137
+ "timestamptz",
138
+ "timestamptz-date"
139
+ ]) sqlApplicationTypes[`pg/${name}@1`] = ["Date"];
140
+ sqlApplicationTypes["sql/timestamp@1"] = ["Date"];
141
+ sqlApplicationTypes["pg/text-array@1"] = ["string[]"];
142
+ sqlApplicationTypes["pg/bytea@1"] = ["Uint8Array"];
143
+ sqlApplicationTypes["pg/json@1"] = ["unknown"];
144
+ sqlApplicationTypes["pg/jsonb@1"] = ["unknown"];
145
+ const defaultValidationCodecs = { targetTypesFor: (id) => idbCodecLookup.targetTypesFor(id) ?? sqlApplicationTypes[id] };
146
+ //#endregion
96
147
  //#region src/core/sync-server.ts
97
148
  /**
98
149
  * Default resolver: duck-types `model.storage.keyPath` as a string. This
@@ -124,16 +175,46 @@ function buildOwnershipCheck(dag, contract, getKeyField, modelName, keyField, ke
124
175
  paths: resolveAuthorizationPaths(contract, dag.rootModel, modelName)
125
176
  };
126
177
  }
127
- function validatePush(dag, contract, getKeyField, events, options) {
178
+ function validatePush(dag, contract, clientContract, getKeyField, events, options) {
128
179
  const models = domainModelsAtDefaultNamespace(contract.domain);
180
+ const clientModels = domainModelsAtDefaultNamespace(clientContract.domain);
129
181
  return events.map((event) => {
130
- if (!dag.clientModels.has(event.model) || !models[event.model]) return {
182
+ if (!dag.clientModels.has(event.model) || !models[event.model] || !clientModels[event.model]) return {
131
183
  eventId: event.id,
132
184
  model: event.model,
133
185
  check: { kind: "unknown-model" }
134
186
  };
135
187
  const keyField = getKeyField(contract, event.model);
136
- const check = buildOwnershipCheck(dag, contract, getKeyField, event.model, keyField, event.payload[keyField], options.scopeKey);
188
+ const codecLookup = options.codecLookup ?? defaultValidationCodecs;
189
+ const keyValue = event.payload?.[keyField];
190
+ const keyResult = validateKeyFields(contract, event.model, { [keyField]: keyValue }, [keyField], { codecLookup });
191
+ if (!keyResult.ok) return {
192
+ eventId: event.id,
193
+ model: event.model,
194
+ check: {
195
+ kind: "validation-failure",
196
+ error: "KEYPATH_VALIDATION_FAILURE",
197
+ issues: keyResult.issues
198
+ }
199
+ };
200
+ if (event.operation !== "delete") {
201
+ const optionalFields = Object.entries(clientModels[event.model].fields).filter(([, field]) => field.nullable).map(([name]) => name);
202
+ const result = validateRecord(clientContract, event.model, event.payload, {
203
+ codecLookup,
204
+ partial: event.operation === "update",
205
+ optionalFields
206
+ });
207
+ if (!result.ok) return {
208
+ eventId: event.id,
209
+ model: event.model,
210
+ check: {
211
+ kind: "validation-failure",
212
+ error: "RECORD_VALIDATION_FAILURE",
213
+ issues: result.issues
214
+ }
215
+ };
216
+ }
217
+ const check = buildOwnershipCheck(dag, contract, getKeyField, event.model, keyField, (event.wirePayload ?? event.payload)[keyField], options.scopeKey);
137
218
  return {
138
219
  eventId: event.id,
139
220
  model: event.model,
@@ -165,13 +246,23 @@ function buildPullQueries(dag, contract, getKeyField, logs, options) {
165
246
  function createSyncServer(options) {
166
247
  const dag = buildOwnershipDag(options.contract, options.clientContract, options.rootModel);
167
248
  const getKeyField = options.getKeyField ?? defaultGetKeyField;
249
+ const codecLookup = options.codecLookup ?? defaultValidationCodecs;
250
+ for (const model of dag.clientModels) try {
251
+ assertRecordValidator(options.clientContract, model, { codecLookup });
252
+ } catch (error) {
253
+ const reason = error instanceof Error ? error.message : "Unable to build record validator";
254
+ throw new Error(`createSyncServer: cannot validate model "${model}": ${reason}. Provide a codecLookup with supported application types or select a Date/string codec. Temporal and interval objects have no default validator.`);
255
+ }
168
256
  return {
169
257
  rootModel: dag.rootModel,
170
- validatePush: (events, pushOptions) => validatePush(dag, options.contract, getKeyField, events, pushOptions),
258
+ validatePush: (events, pushOptions) => validatePush(dag, options.contract, options.clientContract, getKeyField, events, {
259
+ ...pushOptions,
260
+ codecLookup
261
+ }),
171
262
  buildPullQueries: (logs, pullOptions) => buildPullQueries(dag, options.contract, getKeyField, logs, pullOptions)
172
263
  };
173
264
  }
174
265
  //#endregion
175
- export { buildOwnershipDag, buildPullQueries, createSyncServer, defaultGetKeyField, resolveAuthorizationPaths, validatePush };
266
+ export { buildOwnershipDag, buildPullQueries, createSyncServer, defaultGetKeyField, defaultValidationCodecs, resolveAuthorizationPaths, validatePush };
176
267
 
177
268
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","names":[],"sources":["../src/core/ownership-dag.ts","../src/core/authorization-paths.ts","../src/core/sync-server.ts"],"sourcesContent":["import type { Contract } from \"@prisma/orm-framework/contract/types\";\nimport { domainModelsAtDefaultNamespace } from \"@prisma/orm-framework/contract/types\";\n\n/**\n * The contract shape `sync-server` operates on — any family's `Contract`\n * (see ADR 014's \"Genuinely family-agnostic\"). The DAG only ever walks\n * `contract.domain` (relations, model names), which is framework-level and\n * identical in shape across IDB, SQL, Mongo, or any future family — nothing\n * here reads `contract.storage` except through the injectable `getKeyField`\n * resolver in `sync-server.ts`, which is the one place storage shape\n * actually varies by family.\n */\nexport type SyncServerContract = Contract;\n\n/**\n * The ownership graph: `edges.get(model)` is every model reachable from\n * `model` via one of its own `N:1` relations (the models it could be\n * \"owned by\"). Built once, at `createSyncServer()` construction — never\n * per-request (ADR 014).\n */\nexport interface OwnershipDag {\n readonly rootModel: string;\n readonly edges: ReadonlyMap<string, ReadonlySet<string>>;\n /** Model names present in the client-projected contract (ADR 012). */\n readonly clientModels: ReadonlySet<string>;\n}\n\n/**\n * Builds the ownership DAG from the full server-side contract, walking\n * `model.relations` for every domain model. Throws if the graph has a\n * cycle, or if any *client* model (a model that survives ADR 012's\n * projection) has no chain of owning relations back to `rootModel`.\n *\n * Server-only models (present in `contract` but not `clientContract`) are\n * exempt from the reachability requirement — they can never appear in an\n * outbox event or a pull scope check, so there's nothing to authorize. They\n * still count as graph nodes, since a client model's authorization chain\n * may legitimately pass through one (see ADR 014's \"why the full graph\").\n */\nexport function buildOwnershipDag(\n contract: SyncServerContract,\n clientContract: SyncServerContract,\n rootModel: string\n): OwnershipDag {\n const models = domainModelsAtDefaultNamespace(contract.domain);\n const clientModels = new Set(Object.keys(domainModelsAtDefaultNamespace(clientContract.domain)));\n\n if (!(rootModel in models)) {\n throw new Error(`Root model \"${rootModel}\" is not present in the contract's domain models.`);\n }\n if (!clientModels.has(rootModel)) {\n throw new Error(`Root model \"${rootModel}\" is not present in the client contract's domain models.`);\n }\n\n const edges = new Map<string, Set<string>>();\n for (const modelName of Object.keys(models)) {\n edges.set(modelName, new Set());\n }\n for (const [modelName, model] of Object.entries(models)) {\n for (const relation of Object.values(model.relations)) {\n if (relation.cardinality === \"N:1\" && models[relation.to.model]) {\n edges.get(modelName)?.add(relation.to.model);\n }\n }\n }\n\n validateAcyclic(edges);\n validateClientReachability(edges, clientModels, rootModel);\n\n return { rootModel, edges, clientModels };\n}\n\nfunction validateAcyclic(edges: ReadonlyMap<string, ReadonlySet<string>>): void {\n const state = new Map<string, \"visiting\" | \"done\">();\n\n function visit(node: string): void {\n if (state.get(node) === \"done\") return;\n if (state.get(node) === \"visiting\") {\n throw new Error(`Cycle detected in the ownership graph involving model \"${node}\".`);\n }\n state.set(node, \"visiting\");\n for (const neighbor of edges.get(node) ?? []) {\n visit(neighbor);\n }\n state.set(node, \"done\");\n }\n\n for (const node of edges.keys()) {\n visit(node);\n }\n}\n\nfunction validateClientReachability(\n edges: ReadonlyMap<string, ReadonlySet<string>>,\n clientModels: ReadonlySet<string>,\n rootModel: string\n): void {\n const reverseEdges = new Map<string, Set<string>>();\n for (const modelName of edges.keys()) {\n reverseEdges.set(modelName, new Set());\n }\n for (const [modelName, targets] of edges) {\n for (const target of targets) {\n reverseEdges.get(target)?.add(modelName);\n }\n }\n\n const visited = new Set<string>([rootModel]);\n const stack = [rootModel];\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined) continue;\n for (const neighbor of reverseEdges.get(current) ?? []) {\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n stack.push(neighbor);\n }\n }\n }\n\n for (const modelName of clientModels) {\n if (!visited.has(modelName)) {\n throw new Error(\n `Model \"${modelName}\" has no path of owning relations back to root model \"${rootModel}\". ` +\n `Add a relation chain to \"${rootModel}\", or exclude the model from the client contract if it shouldn't sync.`\n );\n }\n }\n}\n","import { domainModelsAtDefaultNamespace } from \"@prisma/orm-framework/contract/types\";\nimport type { SyncServerContract } from \"./ownership-dag\";\n\n/**\n * Every structural path of `N:1` relation names from `modelName` back to\n * `rootModel` — not just the shortest. A record legitimately reachable\n * through relation A *or* relation B is authorized either way (ADR 014's\n * \"any one path, not shortest path\"), so callers must check all of them.\n *\n * Sorted shortest-first, then lexicographically by relation-name chain, for\n * deterministic output — matches the old generator's `buildAllAuthorizationPaths`\n * precedent, minus the \"prefer required\" tiebreak (this DAG doesn't\n * distinguish nullable from required FKs; see ADR 014's rationale).\n */\nexport function resolveAuthorizationPaths(\n contract: SyncServerContract,\n rootModel: string,\n modelName: string\n): readonly (readonly string[])[] {\n if (modelName === rootModel) return [];\n\n const models = domainModelsAtDefaultNamespace(contract.domain);\n const paths: string[][] = [];\n\n const dfs = (current: string, path: string[], visited: Set<string>): void => {\n const model = models[current];\n if (!model) return;\n\n for (const [relationName, relation] of Object.entries(model.relations)) {\n if (relation.cardinality !== \"N:1\") continue;\n const target = relation.to.model;\n if (!models[target] || visited.has(target)) continue;\n\n path.push(relationName);\n if (target === rootModel) {\n paths.push([...path]);\n } else {\n visited.add(target);\n dfs(target, path, visited);\n visited.delete(target);\n }\n path.pop();\n }\n };\n\n dfs(modelName, [], new Set([modelName]));\n\n return paths.sort((a, b) => a.length - b.length || a.join(\".\").localeCompare(b.join(\".\")));\n}\n","import { domainModelsAtDefaultNamespace } from \"@prisma/orm-framework/contract/types\";\nimport { resolveAuthorizationPaths } from \"./authorization-paths\";\nimport { buildOwnershipDag } from \"./ownership-dag\";\nimport type { OwnershipDag, SyncServerContract } from \"./ownership-dag\";\n\n/** An outbox event pending push, transport-agnostic (no browser types). */\nexport interface SyncPushEvent {\n readonly id: string;\n readonly model: string;\n readonly operation: \"create\" | \"update\" | \"delete\";\n readonly payload: Record<string, unknown>;\n}\n\n/**\n * A changelog row pending pull; `key` is the row's own primary-key value.\n *\n * Expected to already be pre-filtered to the caller — e.g. by a `scopeKey`\n * column the caller stamped on the changelog row at push time (reusing the\n * `scopeKey` that authorized it) and queried flatly (`WHERE scopeKey = ?`).\n * `buildPullQueries` doesn't repeat that filter; it re-derives ownership\n * live, since the stamped value is a push-time snapshot and can go stale if\n * the record's ownership chain changes afterward (e.g. a `Board` handed to\n * a different owner after a `Todo` under it was already pushed).\n */\nexport interface SyncPullLogEntry {\n readonly changelogId: string;\n readonly model: string;\n readonly key: unknown;\n}\n\n/**\n * What the caller must check to authorize a record. Never a query result —\n * `sync-server` never touches a database (ADR 014's transport-agnostic\n * boundary). The caller executes the check against whatever storage it uses.\n */\nexport type OwnershipCheck =\n | { readonly kind: \"unknown-model\" }\n | {\n readonly kind: \"root\";\n readonly keyField: string;\n readonly key: unknown;\n readonly scopeKey: string;\n /** The row *is* the root — no query needed, this is already decided. */\n readonly authorized: boolean;\n }\n | {\n readonly kind: \"scoped\";\n readonly keyField: string;\n readonly key: unknown;\n readonly rootKeyField: string;\n readonly scopeKey: string;\n /**\n * Every relation-name chain from this model to `rootModel`. The\n * caller is authorized via *any one* resolving to `scopeKey`\n * (ADR 014) — e.g. `findFirst` with an `OR` across paths.\n */\n readonly paths: readonly (readonly string[])[];\n };\n\nexport interface PushValidationResult {\n readonly eventId: string;\n readonly model: string;\n readonly check: OwnershipCheck;\n}\n\nexport interface PullScopeResult {\n readonly changelogId: string;\n readonly model: string;\n readonly check: OwnershipCheck;\n}\n\n/**\n * Resolves a model's primary-key field name from the contract. Storage\n * shape is the one thing that genuinely varies by family (IDB: a flat\n * `model.storage.keyPath` string; SQL: a possibly-compound\n * `contract.storage.namespaces[ns].entries.table[table].primaryKey.columns`\n * array; Mongo, whatever Mongo does) — so this is the sole extension point\n * `createSyncServer` exposes (`CreateSyncServerOptions.getKeyField`) rather\n * than something the DAG tries to introspect generically.\n */\nexport type GetKeyField = (contract: SyncServerContract, modelName: string) => string;\n\n/**\n * Default resolver: duck-types `model.storage.keyPath` as a string. This\n * happens to be exactly IDB's storage shape, but nothing here imports an\n * IDB type to know that — a family whose storage shape doesn't expose a\n * flat `keyPath` (SQL's compound-capable `primaryKey.columns`, for one)\n * simply won't match, and `createSyncServer` throws asking for an explicit\n * `getKeyField` instead of silently misresolving.\n */\nexport const defaultGetKeyField: GetKeyField = (contract, modelName) => {\n const model = domainModelsAtDefaultNamespace(contract.domain)[modelName];\n const keyPath = (model?.storage as { readonly keyPath?: unknown } | undefined)?.keyPath;\n if (typeof keyPath !== \"string\") {\n const hint = Array.isArray(keyPath)\n ? \"This model has a compound primary key (an array keyPath) — sync-server does not support compound-key \" +\n \"models yet. Pass a getKeyField option to createSyncServer, or keep this model out of the sync scope.\"\n : \"This contract's storage shape isn't IDB's — pass a getKeyField option to createSyncServer.\";\n throw new Error(`Model \"${modelName}\" has no single string storage.keyPath in the contract. ${hint}`);\n }\n return keyPath;\n};\n\nfunction buildOwnershipCheck(\n dag: OwnershipDag,\n contract: SyncServerContract,\n getKeyField: GetKeyField,\n modelName: string,\n keyField: string,\n key: unknown,\n scopeKey: string\n): OwnershipCheck {\n if (modelName === dag.rootModel) {\n return { kind: \"root\", keyField, key, scopeKey, authorized: key === scopeKey };\n }\n return {\n kind: \"scoped\",\n keyField,\n key,\n rootKeyField: getKeyField(contract, dag.rootModel),\n scopeKey,\n paths: resolveAuthorizationPaths(contract, dag.rootModel, modelName),\n };\n}\n\nexport function validatePush(\n dag: OwnershipDag,\n contract: SyncServerContract,\n getKeyField: GetKeyField,\n events: readonly SyncPushEvent[],\n options: { readonly scopeKey: string }\n): readonly PushValidationResult[] {\n const models = domainModelsAtDefaultNamespace(contract.domain);\n\n return events.map((event) => {\n if (!dag.clientModels.has(event.model) || !models[event.model]) {\n return { eventId: event.id, model: event.model, check: { kind: \"unknown-model\" } };\n }\n const keyField = getKeyField(contract, event.model);\n const check = buildOwnershipCheck(\n dag,\n contract,\n getKeyField,\n event.model,\n keyField,\n event.payload[keyField],\n options.scopeKey\n );\n return { eventId: event.id, model: event.model, check };\n });\n}\n\nexport function buildPullQueries(\n dag: OwnershipDag,\n contract: SyncServerContract,\n getKeyField: GetKeyField,\n logs: readonly SyncPullLogEntry[],\n options: { readonly scopeKey: string }\n): readonly PullScopeResult[] {\n const models = domainModelsAtDefaultNamespace(contract.domain);\n\n return logs.map((log) => {\n if (!dag.clientModels.has(log.model) || !models[log.model]) {\n return { changelogId: log.changelogId, model: log.model, check: { kind: \"unknown-model\" } };\n }\n const keyField = getKeyField(contract, log.model);\n const check = buildOwnershipCheck(dag, contract, getKeyField, log.model, keyField, log.key, options.scopeKey);\n return { changelogId: log.changelogId, model: log.model, check };\n });\n}\n\nexport interface CreateSyncServerOptions {\n /** The full server-side contract (ADR 012) — includes client-excluded models. Any family. */\n readonly contract: SyncServerContract;\n /** The client-projected contract (ADR 012) — defines which models are ever synced. Any family. */\n readonly clientContract: SyncServerContract;\n readonly rootModel: string;\n /** @default defaultGetKeyField (IDB-shaped storage.keyPath) */\n readonly getKeyField?: GetKeyField;\n}\n\nexport interface SyncServer {\n readonly rootModel: string;\n /** Resolve every event's ownership check. Pure — never touches a database. */\n validatePush(\n events: readonly SyncPushEvent[],\n options: { readonly scopeKey: string }\n ): readonly PushValidationResult[];\n /** Resolve every changelog row's pull-scope check. Pure — never touches a database. */\n buildPullQueries(\n logs: readonly SyncPullLogEntry[],\n options: { readonly scopeKey: string }\n ): readonly PullScopeResult[];\n}\n\n/**\n * Builds the ownership DAG once (throws on a broken schema — see\n * `buildOwnershipDag`) and returns a `SyncServer` bound to it.\n */\nexport function createSyncServer(options: CreateSyncServerOptions): SyncServer {\n const dag = buildOwnershipDag(options.contract, options.clientContract, options.rootModel);\n const getKeyField = options.getKeyField ?? defaultGetKeyField;\n\n return {\n rootModel: dag.rootModel,\n validatePush: (events, pushOptions) => validatePush(dag, options.contract, getKeyField, events, pushOptions),\n buildPullQueries: (logs, pullOptions) => buildPullQueries(dag, options.contract, getKeyField, logs, pullOptions),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.mjs","names":[],"sources":["../src/core/ownership-dag.ts","../src/core/authorization-paths.ts","../src/core/validation-codecs.ts","../src/core/sync-server.ts"],"sourcesContent":["import type { Contract } from \"@prisma/orm-framework/contract/types\";\nimport { domainModelsAtDefaultNamespace } from \"@prisma/orm-framework/contract/types\";\n\n/**\n * The contract shape `sync-server` operates on — any family's `Contract`\n * (see ADR 014's \"Genuinely family-agnostic\"). The DAG only ever walks\n * `contract.domain` (relations, model names), which is framework-level and\n * identical in shape across IDB, SQL, Mongo, or any future family — nothing\n * here reads `contract.storage` except through the injectable `getKeyField`\n * resolver in `sync-server.ts`, which is the one place storage shape\n * actually varies by family.\n */\nexport type SyncServerContract = Contract;\n\n/**\n * The ownership graph: `edges.get(model)` is every model reachable from\n * `model` via one of its own `N:1` relations (the models it could be\n * \"owned by\"). Built once, at `createSyncServer()` construction — never\n * per-request (ADR 014).\n */\nexport interface OwnershipDag {\n readonly rootModel: string;\n readonly edges: ReadonlyMap<string, ReadonlySet<string>>;\n /** Model names present in the client-projected contract (ADR 012). */\n readonly clientModels: ReadonlySet<string>;\n}\n\n/**\n * Builds the ownership DAG from the full server-side contract, walking\n * `model.relations` for every domain model. Throws if the graph has a\n * cycle, or if any *client* model (a model that survives ADR 012's\n * projection) has no chain of owning relations back to `rootModel`.\n *\n * Server-only models (present in `contract` but not `clientContract`) are\n * exempt from the reachability requirement — they can never appear in an\n * outbox event or a pull scope check, so there's nothing to authorize. They\n * still count as graph nodes, since a client model's authorization chain\n * may legitimately pass through one (see ADR 014's \"why the full graph\").\n */\nexport function buildOwnershipDag(\n contract: SyncServerContract,\n clientContract: SyncServerContract,\n rootModel: string\n): OwnershipDag {\n const models = domainModelsAtDefaultNamespace(contract.domain);\n const clientModels = new Set(Object.keys(domainModelsAtDefaultNamespace(clientContract.domain)));\n\n if (!(rootModel in models)) {\n throw new Error(`Root model \"${rootModel}\" is not present in the contract's domain models.`);\n }\n if (!clientModels.has(rootModel)) {\n throw new Error(`Root model \"${rootModel}\" is not present in the client contract's domain models.`);\n }\n\n const edges = new Map<string, Set<string>>();\n for (const modelName of Object.keys(models)) {\n edges.set(modelName, new Set());\n }\n for (const [modelName, model] of Object.entries(models)) {\n for (const relation of Object.values(model.relations)) {\n if (relation.cardinality === \"N:1\" && models[relation.to.model]) {\n edges.get(modelName)?.add(relation.to.model);\n }\n }\n }\n\n validateAcyclic(edges);\n validateClientReachability(edges, clientModels, rootModel);\n\n return { rootModel, edges, clientModels };\n}\n\nfunction validateAcyclic(edges: ReadonlyMap<string, ReadonlySet<string>>): void {\n const state = new Map<string, \"visiting\" | \"done\">();\n\n function visit(node: string): void {\n if (state.get(node) === \"done\") return;\n if (state.get(node) === \"visiting\") {\n throw new Error(`Cycle detected in the ownership graph involving model \"${node}\".`);\n }\n state.set(node, \"visiting\");\n for (const neighbor of edges.get(node) ?? []) {\n visit(neighbor);\n }\n state.set(node, \"done\");\n }\n\n for (const node of edges.keys()) {\n visit(node);\n }\n}\n\nfunction validateClientReachability(\n edges: ReadonlyMap<string, ReadonlySet<string>>,\n clientModels: ReadonlySet<string>,\n rootModel: string\n): void {\n const reverseEdges = new Map<string, Set<string>>();\n for (const modelName of edges.keys()) {\n reverseEdges.set(modelName, new Set());\n }\n for (const [modelName, targets] of edges) {\n for (const target of targets) {\n reverseEdges.get(target)?.add(modelName);\n }\n }\n\n const visited = new Set<string>([rootModel]);\n const stack = [rootModel];\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined) continue;\n for (const neighbor of reverseEdges.get(current) ?? []) {\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n stack.push(neighbor);\n }\n }\n }\n\n for (const modelName of clientModels) {\n if (!visited.has(modelName)) {\n throw new Error(\n `Model \"${modelName}\" has no path of owning relations back to root model \"${rootModel}\". ` +\n `Add a relation chain to \"${rootModel}\", or exclude the model from the client contract if it shouldn't sync.`\n );\n }\n }\n}\n","import { domainModelsAtDefaultNamespace } from \"@prisma/orm-framework/contract/types\";\nimport type { SyncServerContract } from \"./ownership-dag\";\n\n/**\n * Every structural path of `N:1` relation names from `modelName` back to\n * `rootModel` — not just the shortest. A record legitimately reachable\n * through relation A *or* relation B is authorized either way (ADR 014's\n * \"any one path, not shortest path\"), so callers must check all of them.\n *\n * Sorted shortest-first, then lexicographically by relation-name chain, for\n * deterministic output — matches the old generator's `buildAllAuthorizationPaths`\n * precedent, minus the \"prefer required\" tiebreak (this DAG doesn't\n * distinguish nullable from required FKs; see ADR 014's rationale).\n */\nexport function resolveAuthorizationPaths(\n contract: SyncServerContract,\n rootModel: string,\n modelName: string\n): readonly (readonly string[])[] {\n if (modelName === rootModel) return [];\n\n const models = domainModelsAtDefaultNamespace(contract.domain);\n const paths: string[][] = [];\n\n const dfs = (current: string, path: string[], visited: Set<string>): void => {\n const model = models[current];\n if (!model) return;\n\n for (const [relationName, relation] of Object.entries(model.relations)) {\n if (relation.cardinality !== \"N:1\") continue;\n const target = relation.to.model;\n if (!models[target] || visited.has(target)) continue;\n\n path.push(relationName);\n if (target === rootModel) {\n paths.push([...path]);\n } else {\n visited.add(target);\n dfs(target, path, visited);\n visited.delete(target);\n }\n path.pop();\n }\n };\n\n dfs(modelName, [], new Set([modelName]));\n\n return paths.sort((a, b) => a.length - b.length || a.join(\".\").localeCompare(b.join(\".\")));\n}\n","import { idbCodecLookup } from \"@prisma-idb/target-idb/runtime\";\nimport type { ValidationCodecLookup } from \"@prisma-idb/target-idb/runtime\";\n\n// SQL descriptors' targetTypes are database type names, unlike IDB's JS names.\n// Map codec identities to their application representations, never storage types.\nconst sqlApplicationTypes: Record<string, readonly string[]> = {};\nfor (const name of [\"text\", \"char\", \"varchar\"]) sqlApplicationTypes[`sql/${name}@1`] = [\"string\"];\nfor (const name of [\"int\", \"float\"]) sqlApplicationTypes[`sql/${name}@1`] = [\"number\"];\nfor (const name of [\n \"text\",\n \"enum\",\n \"char\",\n \"varchar\",\n \"uuid\",\n \"numeric\",\n \"inet\",\n \"bit\",\n \"varbit\",\n \"time\",\n \"timetz\",\n \"date-string\",\n \"timestamp-string\",\n \"timestamptz-string\",\n \"time-string\",\n \"tsquery\",\n]) {\n sqlApplicationTypes[`pg/${name}@1`] = [\"string\"];\n}\nfor (const name of [\"int\", \"int2\", \"int4\", \"int8number\", \"float\", \"float4\", \"float8\"]) {\n sqlApplicationTypes[`pg/${name}@1`] = [\"number\"];\n}\nfor (const name of [\"int8\", \"unboundedint\"]) sqlApplicationTypes[`pg/${name}@1`] = [\"bigint\"];\nsqlApplicationTypes[\"pg/bool@1\"] = [\"boolean\"];\n// rc.5 date/timestamp codecs carry Date; pg/time carries string. rc.6 removed\n// these IDs, but historical contracts still need the same native validation.\nfor (const name of [\"date\", \"timestamp\", \"timestamptz\", \"timestamptz-date\"]) {\n sqlApplicationTypes[`pg/${name}@1`] = [\"Date\"];\n}\nsqlApplicationTypes[\"sql/timestamp@1\"] = [\"Date\"];\nsqlApplicationTypes[\"pg/text-array@1\"] = [\"string[]\"];\n// Temporal codecs and pg/interval@1 carry objects, not Date/string. They have\n// no default validator; createSyncServer rejects them before accepting pushes.\nsqlApplicationTypes[\"pg/bytea@1\"] = [\"Uint8Array\"];\nsqlApplicationTypes[\"pg/json@1\"] = [\"unknown\"];\nsqlApplicationTypes[\"pg/jsonb@1\"] = [\"unknown\"];\n\nexport const defaultValidationCodecs: ValidationCodecLookup = {\n targetTypesFor: (id) => idbCodecLookup.targetTypesFor(id) ?? sqlApplicationTypes[id],\n};\n","import { domainModelsAtDefaultNamespace } from \"@prisma/orm-framework/contract/types\";\nimport { assertRecordValidator, validateRecord, validateKeyFields } from \"@prisma-idb/target-idb/runtime\";\nimport type { ValidationCodecLookup } from \"@prisma-idb/target-idb/runtime\";\nimport { defaultValidationCodecs } from \"./validation-codecs\";\nimport { resolveAuthorizationPaths } from \"./authorization-paths\";\nimport { buildOwnershipDag } from \"./ownership-dag\";\nimport type { OwnershipDag, SyncServerContract } from \"./ownership-dag\";\n\n/** An outbox event pending push, transport-agnostic (no browser types). */\nexport interface SyncPushEvent {\n readonly id: string;\n readonly model: string;\n readonly operation: \"create\" | \"update\" | \"delete\";\n /** Native application values used for contract validation. */\n readonly payload: Record<string, unknown>;\n /** Pre-revival payload; supply when native keys differ from their JSON wire form. */\n readonly wirePayload?: Record<string, unknown>;\n}\n\n/**\n * A changelog row pending pull; `key` is the row's own primary-key value.\n *\n * Expected to already be pre-filtered to the caller — e.g. by a `scopeKey`\n * column the caller stamped on the changelog row at push time (reusing the\n * `scopeKey` that authorized it) and queried flatly (`WHERE scopeKey = ?`).\n * `buildPullQueries` doesn't repeat that filter; it re-derives ownership\n * live, since the stamped value is a push-time snapshot and can go stale if\n * the record's ownership chain changes afterward (e.g. a `Board` handed to\n * a different owner after a `Todo` under it was already pushed).\n */\nexport interface SyncPullLogEntry {\n readonly changelogId: string;\n readonly model: string;\n readonly key: unknown;\n}\n\n/**\n * What the caller must check to authorize a record. Never a query result —\n * `sync-server` never touches a database (ADR 014's transport-agnostic\n * boundary). The caller executes the check against whatever storage it uses.\n */\nexport type OwnershipCheck =\n | { readonly kind: \"unknown-model\" }\n | {\n readonly kind: \"root\";\n readonly keyField: string;\n readonly key: unknown;\n readonly scopeKey: string;\n /** The row *is* the root — no query needed, this is already decided. */\n readonly authorized: boolean;\n }\n | {\n readonly kind: \"scoped\";\n readonly keyField: string;\n readonly key: unknown;\n readonly rootKeyField: string;\n readonly scopeKey: string;\n /**\n * Every relation-name chain from this model to `rootModel`. The\n * caller is authorized via *any one* resolving to `scopeKey`\n * (ADR 014) — e.g. `findFirst` with an `OR` across paths.\n */\n readonly paths: readonly (readonly string[])[];\n };\n\nexport type PushCheck =\n | OwnershipCheck\n | {\n readonly kind: \"validation-failure\";\n readonly error: \"RECORD_VALIDATION_FAILURE\" | \"KEYPATH_VALIDATION_FAILURE\";\n readonly issues: readonly string[];\n };\n\nexport interface PushValidationResult {\n readonly eventId: string;\n readonly model: string;\n readonly check: PushCheck;\n}\n\nexport interface PullScopeResult {\n readonly changelogId: string;\n readonly model: string;\n readonly check: OwnershipCheck;\n}\n\n/**\n * Resolves a model's primary-key field name from the contract. Storage\n * shape is the one thing that genuinely varies by family (IDB: a flat\n * `model.storage.keyPath` string; SQL: a possibly-compound\n * `contract.storage.namespaces[ns].entries.table[table].primaryKey.columns`\n * array; Mongo, whatever Mongo does) — so this is the sole extension point\n * `createSyncServer` exposes (`CreateSyncServerOptions.getKeyField`) rather\n * than something the DAG tries to introspect generically.\n */\nexport type GetKeyField = (contract: SyncServerContract, modelName: string) => string;\n\n/**\n * Default resolver: duck-types `model.storage.keyPath` as a string. This\n * happens to be exactly IDB's storage shape, but nothing here imports an\n * IDB type to know that — a family whose storage shape doesn't expose a\n * flat `keyPath` (SQL's compound-capable `primaryKey.columns`, for one)\n * simply won't match, and `createSyncServer` throws asking for an explicit\n * `getKeyField` instead of silently misresolving.\n */\nexport const defaultGetKeyField: GetKeyField = (contract, modelName) => {\n const model = domainModelsAtDefaultNamespace(contract.domain)[modelName];\n const keyPath = (model?.storage as { readonly keyPath?: unknown } | undefined)?.keyPath;\n if (typeof keyPath !== \"string\") {\n const hint = Array.isArray(keyPath)\n ? \"This model has a compound primary key (an array keyPath) — sync-server does not support compound-key \" +\n \"models yet. Pass a getKeyField option to createSyncServer, or keep this model out of the sync scope.\"\n : \"This contract's storage shape isn't IDB's — pass a getKeyField option to createSyncServer.\";\n throw new Error(`Model \"${modelName}\" has no single string storage.keyPath in the contract. ${hint}`);\n }\n return keyPath;\n};\n\nfunction buildOwnershipCheck(\n dag: OwnershipDag,\n contract: SyncServerContract,\n getKeyField: GetKeyField,\n modelName: string,\n keyField: string,\n key: unknown,\n scopeKey: string\n): OwnershipCheck {\n if (modelName === dag.rootModel) {\n return { kind: \"root\", keyField, key, scopeKey, authorized: key === scopeKey };\n }\n return {\n kind: \"scoped\",\n keyField,\n key,\n rootKeyField: getKeyField(contract, dag.rootModel),\n scopeKey,\n paths: resolveAuthorizationPaths(contract, dag.rootModel, modelName),\n };\n}\n\nexport function validatePush(\n dag: OwnershipDag,\n contract: SyncServerContract,\n clientContract: SyncServerContract,\n getKeyField: GetKeyField,\n events: readonly SyncPushEvent[],\n options: { readonly scopeKey: string; readonly codecLookup?: ValidationCodecLookup }\n): readonly PushValidationResult[] {\n const models = domainModelsAtDefaultNamespace(contract.domain);\n const clientModels = domainModelsAtDefaultNamespace(clientContract.domain);\n\n return events.map((event) => {\n if (!dag.clientModels.has(event.model) || !models[event.model] || !clientModels[event.model]) {\n return { eventId: event.id, model: event.model, check: { kind: \"unknown-model\" } };\n }\n const keyField = getKeyField(contract, event.model);\n const codecLookup = options.codecLookup ?? defaultValidationCodecs;\n const keyValue = event.payload?.[keyField];\n const keyResult = validateKeyFields(contract, event.model, { [keyField]: keyValue }, [keyField], { codecLookup });\n if (!keyResult.ok) {\n return {\n eventId: event.id,\n model: event.model,\n check: {\n kind: \"validation-failure\",\n error: \"KEYPATH_VALIDATION_FAILURE\",\n issues: keyResult.issues,\n },\n };\n }\n if (event.operation !== \"delete\") {\n // SQL creates may omit nullable fields; patches validate only supplied fields.\n const optionalFields = Object.entries(clientModels[event.model]!.fields)\n .filter(([, field]) => field.nullable)\n .map(([name]) => name);\n const result = validateRecord(clientContract, event.model, event.payload, {\n codecLookup,\n partial: event.operation === \"update\",\n optionalFields,\n });\n if (!result.ok) {\n return {\n eventId: event.id,\n model: event.model,\n check: {\n kind: \"validation-failure\",\n error: \"RECORD_VALIDATION_FAILURE\",\n issues: result.issues,\n },\n };\n }\n }\n const check = buildOwnershipCheck(\n dag,\n contract,\n getKeyField,\n event.model,\n keyField,\n (event.wirePayload ?? event.payload)[keyField],\n options.scopeKey\n );\n return { eventId: event.id, model: event.model, check };\n });\n}\n\nexport function buildPullQueries(\n dag: OwnershipDag,\n contract: SyncServerContract,\n getKeyField: GetKeyField,\n logs: readonly SyncPullLogEntry[],\n options: { readonly scopeKey: string }\n): readonly PullScopeResult[] {\n const models = domainModelsAtDefaultNamespace(contract.domain);\n\n return logs.map((log) => {\n if (!dag.clientModels.has(log.model) || !models[log.model]) {\n return { changelogId: log.changelogId, model: log.model, check: { kind: \"unknown-model\" } };\n }\n const keyField = getKeyField(contract, log.model);\n const check = buildOwnershipCheck(dag, contract, getKeyField, log.model, keyField, log.key, options.scopeKey);\n return { changelogId: log.changelogId, model: log.model, check };\n });\n}\n\nexport interface CreateSyncServerOptions {\n /** The full server-side contract (ADR 012) — includes client-excluded models. Any family. */\n readonly contract: SyncServerContract;\n /** The client-projected contract (ADR 012) — defines the synced models and fields to validate. Any family. */\n readonly clientContract: SyncServerContract;\n readonly rootModel: string;\n /** @default defaultGetKeyField (IDB-shaped storage.keyPath) */\n readonly getKeyField?: GetKeyField;\n /** Native application types; unsupported validators throw at construction. */\n readonly codecLookup?: ValidationCodecLookup;\n}\n\nexport interface SyncServer {\n readonly rootModel: string;\n /** Resolve every event's ownership check. Pure — never touches a database. */\n validatePush(\n events: readonly SyncPushEvent[],\n options: { readonly scopeKey: string }\n ): readonly PushValidationResult[];\n /** Resolve every changelog row's pull-scope check. Pure — never touches a database. */\n buildPullQueries(\n logs: readonly SyncPullLogEntry[],\n options: { readonly scopeKey: string }\n ): readonly PullScopeResult[];\n}\n\n/**\n * Builds the ownership DAG once (throws on a broken schema — see\n * `buildOwnershipDag`) and returns a `SyncServer` bound to it.\n */\nexport function createSyncServer(options: CreateSyncServerOptions): SyncServer {\n const dag = buildOwnershipDag(options.contract, options.clientContract, options.rootModel);\n const getKeyField = options.getKeyField ?? defaultGetKeyField;\n const codecLookup = options.codecLookup ?? defaultValidationCodecs;\n for (const model of dag.clientModels) {\n try {\n assertRecordValidator(options.clientContract, model, { codecLookup });\n } catch (error) {\n const reason = error instanceof Error ? error.message : \"Unable to build record validator\";\n throw new Error(\n `createSyncServer: cannot validate model \"${model}\": ${reason}. ` +\n \"Provide a codecLookup with supported application types or select a Date/string codec. \" +\n \"Temporal and interval objects have no default validator.\"\n );\n }\n }\n\n return {\n rootModel: dag.rootModel,\n validatePush: (events, pushOptions) =>\n validatePush(dag, options.contract, options.clientContract, getKeyField, events, {\n ...pushOptions,\n codecLookup,\n }),\n buildPullQueries: (logs, pullOptions) => buildPullQueries(dag, options.contract, getKeyField, logs, pullOptions),\n };\n}\n"],"mappings":"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"}
@@ -13,9 +13,13 @@ import { prismaContract } from "@prisma/orm-family-sql/contract-psl/provider";
13
13
  * the old generator required an exact 6-field model + a matching enum and
14
14
  * threw one of a dozen validation errors if either drifted.
15
15
  *
16
- * `id` is a real DB-generated `autoincrement()` — no client ever creates a
17
- * `Changelog` row, this is server-only bookkeeping, so there's no reason
18
- * to hand it a client-style id. `keyPath` is `String`, not `Json`: every
16
+ * `id` is a UUID v7 (`uuid(7)`), matching the old generator: fixed-width
17
+ * and time-ordered, so it sorts correctly as a plain string — the pull
18
+ * cursor stays an opaque string on the wire and never needs numeric
19
+ * comparison or an int range check. It is generated by the ORM's mutation
20
+ * default at insert time, not by the database. That ordering is by insert
21
+ * time, not commit time, so concurrent pushes can still commit out of order
22
+ * (pull's gap, tracked separately) — the id doesn't change that. `keyPath` is `String`, not `Json`: every
19
23
  * model this package's own consumers sync uses a single-field `String`
20
24
  * key today (no compound keys), so there's nothing to store as JSON, and
21
25
  * — confirmed directly against a real Postgres run — this target's
@@ -39,7 +43,7 @@ import { prismaContract } from "@prisma/orm-family-sql/contract-psl/provider";
39
43
  * `*Sql`, not `*Postgres`, so it shouldn't assume one. This doesn't make it
40
44
  * Mongo-portable, to be clear — Mongo's contract-psl is a different PSL
41
45
  * dialect entirely (different scalar types, presumably no `enum`/
42
- * `@default(autoincrement())` the way SQL has them), so this function was
46
+ * `@default(uuid(7))` the way SQL has them), so this function was
43
47
  * never going to apply there regardless; the fix here is about staying
44
48
  * agnostic *within* the SQL family (Postgres/SQLite/whatever comes next),
45
49
  * which is the scope its name actually promises.
@@ -52,7 +56,7 @@ enum ChangeOperation {
52
56
  }
53
57
 
54
58
  model Changelog {
55
- id Int @id @default(autoincrement())
59
+ id String @id @default(uuid(7))
56
60
  model String
57
61
  keyPath String
58
62
  operation ChangeOperation
@@ -154,4 +158,4 @@ function sqlContractWithSync(schemaPath, options) {
154
158
  //#endregion
155
159
  export { injectChangelogModelSql as n, prepareSqlSchemaWithSync as r, sqlContractWithSync as t };
156
160
 
157
- //# sourceMappingURL=sql-contract-0ZmfUgDg.mjs.map
161
+ //# sourceMappingURL=sql-contract-DVUxPtOh.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-contract-DVUxPtOh.mjs","names":[],"sources":["../src/core/changelog-schema.ts","../src/core/sql-contract.ts"],"sourcesContent":["import { stripIdbExcludeAttributes } from \"@prisma-idb/family-idb/contract-psl\";\n\n/**\n * Synthetic `Changelog` model, appended to the raw PSL text of a server\n * contract (never the client one) so a consumer never hand-authors it —\n * the old generator required an exact 6-field model + a matching enum and\n * threw one of a dozen validation errors if either drifted.\n *\n * `id` is a UUID v7 (`uuid(7)`), matching the old generator: fixed-width\n * and time-ordered, so it sorts correctly as a plain string — the pull\n * cursor stays an opaque string on the wire and never needs numeric\n * comparison or an int range check. It is generated by the ORM's mutation\n * default at insert time, not by the database. That ordering is by insert\n * time, not commit time, so concurrent pushes can still commit out of order\n * (pull's gap, tracked separately) — the id doesn't change that. `keyPath` is `String`, not `Json`: every\n * model this package's own consumers sync uses a single-field `String`\n * key today (no compound keys), so there's nothing to store as JSON, and\n * — confirmed directly against a real Postgres run — this target's\n * `pg/jsonb@1` codec doesn't decode correctly in the currently-published\n * `@prisma/orm-postgres` version (every other codec, including the enum,\n * decodes fine). `String` sidesteps a real gap rather than working around\n * it blindly. If a future consumer needs a genuinely compound/JSON-shaped\n * key, this is the function to fork, not patch — it'd change\n * `sync-server`'s own `key: unknown` handling too, not just this schema\n * fragment.\n *\n * The enum deliberately has no `@@type(...)` codec pragma — confirmed\n * empirically (a throwaway `contract emit` against `@prisma/orm-postgres`)\n * that omitting it still resolves to `pg/text@1`, because\n * `@prisma/orm-postgres/config`'s `defineConfig` already sets\n * `enumInferenceCodecs: { text: PG_TEXT_CODEC_ID, ... }` — the *target*\n * supplies that default, not the schema text. Hardcoding `@@type(\"pg/text@1\")`\n * here would have been redundant against Postgres and actively wrong\n * against a different SQL target (e.g. a future SQLite one, whose\n * own inference default isn't a `pg/*` codec) — this function is named\n * `*Sql`, not `*Postgres`, so it shouldn't assume one. This doesn't make it\n * Mongo-portable, to be clear — Mongo's contract-psl is a different PSL\n * dialect entirely (different scalar types, presumably no `enum`/\n * `@default(uuid(7))` the way SQL has them), so this function was\n * never going to apply there regardless; the fix here is about staying\n * agnostic *within* the SQL family (Postgres/SQLite/whatever comes next),\n * which is the scope its name actually promises.\n */\nconst CHANGELOG_MODEL_SQL_PSL = `\nenum ChangeOperation {\n create\n update\n delete\n}\n\nmodel Changelog {\n id String @id @default(uuid(7))\n model String\n keyPath String\n operation ChangeOperation\n scopeKey String\n outboxEventId String @unique\n createdAt DateTime @default(now())\n\n @@index([scopeKey, id])\n}\n`;\n\n/** Appends the synthetic `Changelog` model to raw PSL schema text. */\nexport function injectChangelogModelSql(schema: string): string {\n return `${schema}\\n${CHANGELOG_MODEL_SQL_PSL}`;\n}\n\n/**\n * The composed step a SQL-family config needs to turn a schema authored\n * for `family-idb` (browser client) into the real server schema: strip\n * `@idb.exclude`/`@@idb.exclude` (meaningless to a real server, and the\n * SQL parser hard-errors on the unrecognized `idb` namespace otherwise —\n * see `stripIdbExcludeAttributes`), then append `Changelog`. See\n * `sqlContractWithSync` for the version that also feeds this straight into\n * the SQL family's own parser, in memory.\n */\nexport function prepareSqlSchemaWithSync(schema: string): string {\n return injectChangelogModelSql(stripIdbExcludeAttributes(schema));\n}\n","import { readFile } from \"node:fs/promises\";\nimport type { ContractConfig } from \"@prisma/orm-framework/config/config-types\";\nimport { buildSymbolTable, mapPslDiagnostics } from \"@prisma/orm-framework/psl-parser\";\nimport { hasPslInterpreter, withSeedDiagnostics } from \"@prisma/orm-framework/psl-parser/interpret\";\nimport { parse } from \"@prisma/orm-framework/psl-parser/syntax\";\nimport { notOk, ok } from \"@prisma/orm-framework/utils/result\";\nimport { applySqlSpecifierControlPolicy } from \"@prisma/orm-family-sql/contract-ts/contract-builder\";\nimport { prismaContract, type PrismaContractOptions } from \"@prisma/orm-family-sql/contract-psl/provider\";\nimport { prepareSqlSchemaWithSync } from \"./changelog-schema\";\n\n/**\n * Reads `schemaPath` once, runs it through `prepareSqlSchemaWithSync`, and\n * hands the *text* straight to the SQL family's own parse → interpret →\n * control-policy pipeline — no generated `.prisma` file lands on disk.\n *\n * `@prisma/orm-family-sql`'s own `prismaContract(schemaPath, options)`\n * always `readFile()`s its argument, with no hook to inject text instead\n * (github.com/prisma/orm#30115). This works around that today by\n * decomposing it: call the real `prismaContract` to get a `source` whose\n * `interpret` is the target-specific closure we don't want to reimplement\n * (`options.target`, `options.createNamespace`, etc. all live there), then\n * replace only `load` with a version that transforms the schema text\n * in-memory instead of pointing at a second file. The transformed text\n * still flows through the same parse → symbol-table → interpret →\n * `applySqlSpecifierControlPolicy` steps `prismaContract` itself runs, so\n * `storageHash` and diagnostics behave identically to the file-based path.\n *\n * Note: `@prisma/orm-postgres/config`'s `defineConfig` (and its per-target\n * siblings) only accept a schema *path* for `contract`, not a\n * `ContractConfig` — they build their own `prismaContract(...)` call\n * internally. Passing this function's return value to one of those still\n * requires wiring the family/target/adapter/driver through the core\n * `defineConfig` (`@prisma/orm-framework/config/config-types`) directly\n * instead of the target's convenience wrapper.\n */\nexport function sqlContractWithSync(schemaPath: string, options: PrismaContractOptions): ContractConfig {\n const { source, output } = prismaContract(schemaPath, options);\n if (!hasPslInterpreter(source)) {\n throw new Error(\"sqlContractWithSync: prismaContract() did not return a PSL-capable source.\");\n }\n const { interpret } = source;\n\n return {\n ...(output !== undefined ? { output } : {}),\n source: {\n ...source,\n async load(context) {\n const [absoluteSchemaPath] = context.resolvedInputs;\n if (absoluteSchemaPath === undefined) {\n throw new Error(\n \"sqlContractWithSync: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.\"\n );\n }\n\n let raw: string;\n try {\n raw = await readFile(absoluteSchemaPath, \"utf-8\");\n } catch (error) {\n const message = String(error);\n return notOk({\n summary: `Failed to read Prisma schema at \"${schemaPath}\"`,\n diagnostics: [\n {\n code: \"PSL_SCHEMA_READ_FAILED\",\n message,\n sourceId: schemaPath,\n },\n ],\n });\n }\n\n const schema = prepareSqlSchemaWithSync(raw);\n\n const { document, sources, diagnostics: parseDiagnostics } = parse(schema, schemaPath);\n const documents = [document];\n const { symbolTable, diagnostics: symbolTableDiagnostics } = buildSymbolTable({\n documents,\n sources,\n pslBlockDescriptors: context.authoringContributions.pslBlockDescriptors,\n });\n\n const seedDiagnostics = mapPslDiagnostics([...parseDiagnostics, ...symbolTableDiagnostics], sources);\n\n const interpreted = withSeedDiagnostics(\n interpret({ documents, sources, symbolTable }, context),\n seedDiagnostics\n );\n if (!interpreted.ok) {\n return interpreted;\n }\n\n return ok(\n applySqlSpecifierControlPolicy(interpreted.value, options.defaultControlPolicy, options.createNamespace)\n );\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;AAqBhC,SAAgB,wBAAwB,QAAwB;CAC9D,OAAO,GAAG,OAAO,IAAI;AACvB;;;;;;;;;;AAWA,SAAgB,yBAAyB,QAAwB;CAC/D,OAAO,wBAAwB,0BAA0B,MAAM,CAAC;AAClE;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC5CA,SAAgB,oBAAoB,YAAoB,SAAgD;CACtG,MAAM,EAAE,QAAQ,WAAW,eAAe,YAAY,OAAO;CAC7D,IAAI,CAAC,kBAAkB,MAAM,GAC3B,MAAM,IAAI,MAAM,4EAA4E;CAE9F,MAAM,EAAE,cAAc;CAEtB,OAAO;EACL,GAAI,WAAW,KAAA,IAAY,EAAE,OAAO,IAAI,CAAC;EACzC,QAAQ;GACN,GAAG;GACH,MAAM,KAAK,SAAS;IAClB,MAAM,CAAC,sBAAsB,QAAQ;IACrC,IAAI,uBAAuB,KAAA,GACzB,MAAM,IAAI,MACR,uIACF;IAGF,IAAI;IACJ,IAAI;KACF,MAAM,MAAM,SAAS,oBAAoB,OAAO;IAClD,SAAS,OAAO;KACd,MAAM,UAAU,OAAO,KAAK;KAC5B,OAAO,MAAM;MACX,SAAS,oCAAoC,WAAW;MACxD,aAAa,CACX;OACE,MAAM;OACN;OACA,UAAU;MACZ,CACF;KACF,CAAC;IACH;IAEA,MAAM,SAAS,yBAAyB,GAAG;IAE3C,MAAM,EAAE,UAAU,SAAS,aAAa,qBAAqB,MAAM,QAAQ,UAAU;IACrF,MAAM,YAAY,CAAC,QAAQ;IAC3B,MAAM,EAAE,aAAa,aAAa,2BAA2B,iBAAiB;KAC5E;KACA;KACA,qBAAqB,QAAQ,uBAAuB;IACtD,CAAC;IAED,MAAM,kBAAkB,kBAAkB,CAAC,GAAG,kBAAkB,GAAG,sBAAsB,GAAG,OAAO;IAEnG,MAAM,cAAc,oBAClB,UAAU;KAAE;KAAW;KAAS;IAAY,GAAG,OAAO,GACtD,eACF;IACA,IAAI,CAAC,YAAY,IACf,OAAO;IAGT,OAAO,GACL,+BAA+B,YAAY,OAAO,QAAQ,sBAAsB,QAAQ,eAAe,CACzG;GACF;EACF;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma-idb/sync-server",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Prisma 8 IDB — sync ownership DAG (server-side push authorization + pull scoping, ADR 014)",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -41,13 +41,13 @@
41
41
  "./package.json": "./package.json"
42
42
  },
43
43
  "dependencies": {
44
- "@prisma-idb/family-idb": "0.8.0",
44
+ "@prisma-idb/family-idb": "0.9.0",
45
45
  "@prisma/orm-family-sql": "8.0.0-rc.12",
46
46
  "@prisma/orm-framework": "8.0.0-rc.12",
47
- "@prisma/orm-postgres": "8.0.0-rc.12"
47
+ "@prisma/orm-postgres": "8.0.0-rc.12",
48
+ "@prisma-idb/target-idb": "0.9.0"
48
49
  },
49
50
  "devDependencies": {
50
- "@prisma-idb/target-idb": "0.8.0",
51
51
  "@types/node": "^24.13.3",
52
52
  "tsdown": "^0.22.14",
53
53
  "typescript": "^6.0.3",
@@ -1 +0,0 @@
1
- {"version":3,"file":"sql-contract-0ZmfUgDg.mjs","names":[],"sources":["../src/core/changelog-schema.ts","../src/core/sql-contract.ts"],"sourcesContent":["import { stripIdbExcludeAttributes } from \"@prisma-idb/family-idb/contract-psl\";\n\n/**\n * Synthetic `Changelog` model, appended to the raw PSL text of a server\n * contract (never the client one) so a consumer never hand-authors it —\n * the old generator required an exact 6-field model + a matching enum and\n * threw one of a dozen validation errors if either drifted.\n *\n * `id` is a real DB-generated `autoincrement()` — no client ever creates a\n * `Changelog` row, this is server-only bookkeeping, so there's no reason\n * to hand it a client-style id. `keyPath` is `String`, not `Json`: every\n * model this package's own consumers sync uses a single-field `String`\n * key today (no compound keys), so there's nothing to store as JSON, and\n * — confirmed directly against a real Postgres run — this target's\n * `pg/jsonb@1` codec doesn't decode correctly in the currently-published\n * `@prisma/orm-postgres` version (every other codec, including the enum,\n * decodes fine). `String` sidesteps a real gap rather than working around\n * it blindly. If a future consumer needs a genuinely compound/JSON-shaped\n * key, this is the function to fork, not patch — it'd change\n * `sync-server`'s own `key: unknown` handling too, not just this schema\n * fragment.\n *\n * The enum deliberately has no `@@type(...)` codec pragma — confirmed\n * empirically (a throwaway `contract emit` against `@prisma/orm-postgres`)\n * that omitting it still resolves to `pg/text@1`, because\n * `@prisma/orm-postgres/config`'s `defineConfig` already sets\n * `enumInferenceCodecs: { text: PG_TEXT_CODEC_ID, ... }` — the *target*\n * supplies that default, not the schema text. Hardcoding `@@type(\"pg/text@1\")`\n * here would have been redundant against Postgres and actively wrong\n * against a different SQL target (e.g. a future SQLite one, whose\n * own inference default isn't a `pg/*` codec) — this function is named\n * `*Sql`, not `*Postgres`, so it shouldn't assume one. This doesn't make it\n * Mongo-portable, to be clear — Mongo's contract-psl is a different PSL\n * dialect entirely (different scalar types, presumably no `enum`/\n * `@default(autoincrement())` the way SQL has them), so this function was\n * never going to apply there regardless; the fix here is about staying\n * agnostic *within* the SQL family (Postgres/SQLite/whatever comes next),\n * which is the scope its name actually promises.\n */\nconst CHANGELOG_MODEL_SQL_PSL = `\nenum ChangeOperation {\n create\n update\n delete\n}\n\nmodel Changelog {\n id Int @id @default(autoincrement())\n model String\n keyPath String\n operation ChangeOperation\n scopeKey String\n outboxEventId String @unique\n createdAt DateTime @default(now())\n\n @@index([scopeKey, id])\n}\n`;\n\n/** Appends the synthetic `Changelog` model to raw PSL schema text. */\nexport function injectChangelogModelSql(schema: string): string {\n return `${schema}\\n${CHANGELOG_MODEL_SQL_PSL}`;\n}\n\n/**\n * The composed step a SQL-family config needs to turn a schema authored\n * for `family-idb` (browser client) into the real server schema: strip\n * `@idb.exclude`/`@@idb.exclude` (meaningless to a real server, and the\n * SQL parser hard-errors on the unrecognized `idb` namespace otherwise —\n * see `stripIdbExcludeAttributes`), then append `Changelog`. See\n * `sqlContractWithSync` for the version that also feeds this straight into\n * the SQL family's own parser, in memory.\n */\nexport function prepareSqlSchemaWithSync(schema: string): string {\n return injectChangelogModelSql(stripIdbExcludeAttributes(schema));\n}\n","import { readFile } from \"node:fs/promises\";\nimport type { ContractConfig } from \"@prisma/orm-framework/config/config-types\";\nimport { buildSymbolTable, mapPslDiagnostics } from \"@prisma/orm-framework/psl-parser\";\nimport { hasPslInterpreter, withSeedDiagnostics } from \"@prisma/orm-framework/psl-parser/interpret\";\nimport { parse } from \"@prisma/orm-framework/psl-parser/syntax\";\nimport { notOk, ok } from \"@prisma/orm-framework/utils/result\";\nimport { applySqlSpecifierControlPolicy } from \"@prisma/orm-family-sql/contract-ts/contract-builder\";\nimport { prismaContract, type PrismaContractOptions } from \"@prisma/orm-family-sql/contract-psl/provider\";\nimport { prepareSqlSchemaWithSync } from \"./changelog-schema\";\n\n/**\n * Reads `schemaPath` once, runs it through `prepareSqlSchemaWithSync`, and\n * hands the *text* straight to the SQL family's own parse → interpret →\n * control-policy pipeline — no generated `.prisma` file lands on disk.\n *\n * `@prisma/orm-family-sql`'s own `prismaContract(schemaPath, options)`\n * always `readFile()`s its argument, with no hook to inject text instead\n * (github.com/prisma/orm#30115). This works around that today by\n * decomposing it: call the real `prismaContract` to get a `source` whose\n * `interpret` is the target-specific closure we don't want to reimplement\n * (`options.target`, `options.createNamespace`, etc. all live there), then\n * replace only `load` with a version that transforms the schema text\n * in-memory instead of pointing at a second file. The transformed text\n * still flows through the same parse → symbol-table → interpret →\n * `applySqlSpecifierControlPolicy` steps `prismaContract` itself runs, so\n * `storageHash` and diagnostics behave identically to the file-based path.\n *\n * Note: `@prisma/orm-postgres/config`'s `defineConfig` (and its per-target\n * siblings) only accept a schema *path* for `contract`, not a\n * `ContractConfig` — they build their own `prismaContract(...)` call\n * internally. Passing this function's return value to one of those still\n * requires wiring the family/target/adapter/driver through the core\n * `defineConfig` (`@prisma/orm-framework/config/config-types`) directly\n * instead of the target's convenience wrapper.\n */\nexport function sqlContractWithSync(schemaPath: string, options: PrismaContractOptions): ContractConfig {\n const { source, output } = prismaContract(schemaPath, options);\n if (!hasPslInterpreter(source)) {\n throw new Error(\"sqlContractWithSync: prismaContract() did not return a PSL-capable source.\");\n }\n const { interpret } = source;\n\n return {\n ...(output !== undefined ? { output } : {}),\n source: {\n ...source,\n async load(context) {\n const [absoluteSchemaPath] = context.resolvedInputs;\n if (absoluteSchemaPath === undefined) {\n throw new Error(\n \"sqlContractWithSync: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.\"\n );\n }\n\n let raw: string;\n try {\n raw = await readFile(absoluteSchemaPath, \"utf-8\");\n } catch (error) {\n const message = String(error);\n return notOk({\n summary: `Failed to read Prisma schema at \"${schemaPath}\"`,\n diagnostics: [\n {\n code: \"PSL_SCHEMA_READ_FAILED\",\n message,\n sourceId: schemaPath,\n },\n ],\n });\n }\n\n const schema = prepareSqlSchemaWithSync(raw);\n\n const { document, sources, diagnostics: parseDiagnostics } = parse(schema, schemaPath);\n const documents = [document];\n const { symbolTable, diagnostics: symbolTableDiagnostics } = buildSymbolTable({\n documents,\n sources,\n pslBlockDescriptors: context.authoringContributions.pslBlockDescriptors,\n });\n\n const seedDiagnostics = mapPslDiagnostics([...parseDiagnostics, ...symbolTableDiagnostics], sources);\n\n const interpreted = withSeedDiagnostics(\n interpret({ documents, sources, symbolTable }, context),\n seedDiagnostics\n );\n if (!interpreted.ok) {\n return interpreted;\n }\n\n return ok(\n applySqlSpecifierControlPolicy(interpreted.value, options.defaultControlPolicy, options.createNamespace)\n );\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;AAqBhC,SAAgB,wBAAwB,QAAwB;CAC9D,OAAO,GAAG,OAAO,IAAI;AACvB;;;;;;;;;;AAWA,SAAgB,yBAAyB,QAAwB;CAC/D,OAAO,wBAAwB,0BAA0B,MAAM,CAAC;AAClE;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxCA,SAAgB,oBAAoB,YAAoB,SAAgD;CACtG,MAAM,EAAE,QAAQ,WAAW,eAAe,YAAY,OAAO;CAC7D,IAAI,CAAC,kBAAkB,MAAM,GAC3B,MAAM,IAAI,MAAM,4EAA4E;CAE9F,MAAM,EAAE,cAAc;CAEtB,OAAO;EACL,GAAI,WAAW,KAAA,IAAY,EAAE,OAAO,IAAI,CAAC;EACzC,QAAQ;GACN,GAAG;GACH,MAAM,KAAK,SAAS;IAClB,MAAM,CAAC,sBAAsB,QAAQ;IACrC,IAAI,uBAAuB,KAAA,GACzB,MAAM,IAAI,MACR,uIACF;IAGF,IAAI;IACJ,IAAI;KACF,MAAM,MAAM,SAAS,oBAAoB,OAAO;IAClD,SAAS,OAAO;KACd,MAAM,UAAU,OAAO,KAAK;KAC5B,OAAO,MAAM;MACX,SAAS,oCAAoC,WAAW;MACxD,aAAa,CACX;OACE,MAAM;OACN;OACA,UAAU;MACZ,CACF;KACF,CAAC;IACH;IAEA,MAAM,SAAS,yBAAyB,GAAG;IAE3C,MAAM,EAAE,UAAU,SAAS,aAAa,qBAAqB,MAAM,QAAQ,UAAU;IACrF,MAAM,YAAY,CAAC,QAAQ;IAC3B,MAAM,EAAE,aAAa,aAAa,2BAA2B,iBAAiB;KAC5E;KACA;KACA,qBAAqB,QAAQ,uBAAuB;IACtD,CAAC;IAED,MAAM,kBAAkB,kBAAkB,CAAC,GAAG,kBAAkB,GAAG,sBAAsB,GAAG,OAAO;IAEnG,MAAM,cAAc,oBAClB,UAAU;KAAE;KAAW;KAAS;IAAY,GAAG,OAAO,GACtD,eACF;IACA,IAAI,CAAC,YAAY,IACf,OAAO;IAGT,OAAO,GACL,+BAA+B,YAAY,OAAO,QAAQ,sBAAsB,QAAQ,eAAe,CACzG;GACF;EACF;CACF;AACF"}