@shivaedev/effect-prisma 0.5.3 → 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.
Files changed (87) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +25 -7
  3. package/dist/database.d.ts +6 -6
  4. package/dist/database.d.ts.map +1 -1
  5. package/dist/database.js +9 -2
  6. package/dist/database.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/database-factory.d.ts +20 -22
  11. package/dist/internal/database-factory.d.ts.map +1 -1
  12. package/dist/internal/database-factory.js +83 -24
  13. package/dist/internal/database-factory.js.map +1 -1
  14. package/dist/internal/executor.d.ts +9 -7
  15. package/dist/internal/executor.d.ts.map +1 -1
  16. package/dist/internal/query-execution.d.ts +3 -4
  17. package/dist/internal/query-execution.d.ts.map +1 -1
  18. package/dist/internal/query-execution.js +9 -3
  19. package/dist/internal/query-execution.js.map +1 -1
  20. package/dist/internal/recipe.d.ts +1 -1
  21. package/dist/internal/recipe.d.ts.map +1 -1
  22. package/dist/internal/recipe.js +22 -9
  23. package/dist/internal/recipe.js.map +1 -1
  24. package/dist/internal/relation-plan.d.ts +6 -1
  25. package/dist/internal/relation-plan.d.ts.map +1 -1
  26. package/dist/internal/relation-plan.js +6 -5
  27. package/dist/internal/relation-plan.js.map +1 -1
  28. package/dist/internal/relation-runtime.d.ts +4 -3
  29. package/dist/internal/relation-runtime.d.ts.map +1 -1
  30. package/dist/internal/relation-runtime.js +38 -19
  31. package/dist/internal/relation-runtime.js.map +1 -1
  32. package/dist/internal/relation-stream.d.ts +3 -4
  33. package/dist/internal/relation-stream.d.ts.map +1 -1
  34. package/dist/internal/relation-stream.js +23 -21
  35. package/dist/internal/relation-stream.js.map +1 -1
  36. package/dist/internal/testing.d.ts +5 -5
  37. package/dist/internal/testing.d.ts.map +1 -1
  38. package/dist/internal/testing.js.map +1 -1
  39. package/dist/internal/transaction.d.ts +6 -3
  40. package/dist/internal/transaction.d.ts.map +1 -1
  41. package/dist/internal/transaction.js +14 -4
  42. package/dist/internal/transaction.js.map +1 -1
  43. package/dist/internal/vitest-database.d.ts +2 -2
  44. package/dist/internal/vitest-database.d.ts.map +1 -1
  45. package/dist/internal/vitest-database.js.map +1 -1
  46. package/dist/relation/include.d.ts +10 -8
  47. package/dist/relation/include.d.ts.map +1 -1
  48. package/dist/relation/prisma-methods.d.ts +20 -20
  49. package/dist/relation/prisma-methods.d.ts.map +1 -1
  50. package/dist/relation.d.ts +8 -7
  51. package/dist/relation.d.ts.map +1 -1
  52. package/dist/sqlite.d.ts +6 -6
  53. package/dist/sqlite.d.ts.map +1 -1
  54. package/dist/sqlite.js +10 -2
  55. package/dist/sqlite.js.map +1 -1
  56. package/dist/testing/transaction.d.ts +3 -3
  57. package/dist/testing/transaction.d.ts.map +1 -1
  58. package/dist/testing/transaction.js.map +1 -1
  59. package/dist/testing/types.d.ts +1 -1
  60. package/dist/testing/types.d.ts.map +1 -1
  61. package/dist/testing/vitest.d.ts +3 -3
  62. package/dist/testing/vitest.d.ts.map +1 -1
  63. package/dist/testing/vitest.js.map +1 -1
  64. package/dist/testing.d.ts +1 -1
  65. package/dist/testing.d.ts.map +1 -1
  66. package/dist/testing.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/database.ts +53 -47
  69. package/src/index.ts +0 -1
  70. package/src/internal/database-factory.ts +221 -98
  71. package/src/internal/executor.ts +9 -15
  72. package/src/internal/query-execution.ts +14 -6
  73. package/src/internal/recipe.ts +46 -7
  74. package/src/internal/relation-plan.ts +11 -11
  75. package/src/internal/relation-runtime.ts +67 -65
  76. package/src/internal/relation-stream.ts +54 -48
  77. package/src/internal/testing.ts +8 -8
  78. package/src/internal/transaction.ts +80 -56
  79. package/src/internal/vitest-database.ts +3 -12
  80. package/src/relation/include.ts +31 -22
  81. package/src/relation/prisma-methods.ts +27 -31
  82. package/src/relation.ts +19 -27
  83. package/src/sqlite.ts +61 -54
  84. package/src/testing/transaction.ts +16 -6
  85. package/src/testing/types.ts +1 -1
  86. package/src/testing/vitest.ts +3 -10
  87. package/src/testing.ts +0 -1
@@ -1,13 +1,9 @@
1
1
  import type { orm } from "@prisma-next/sql-orm-client";
2
2
  import { Context, Effect, Layer } from "effect";
3
- import type { PrismaError } from "../error.js";
3
+ import { type PrismaError, toPrismaError } from "../error.js";
4
4
  import type { Relation } from "../relation.js";
5
5
  import { assertAvailableModelNames } from "./client-lifecycle.js";
6
- import type {
7
- AnySqlContract,
8
- DatabaseExecutor,
9
- ExecutorIdentifier,
10
- } from "./executor.js";
6
+ import type { AnySqlContract, DatabaseExecutor } from "./executor.js";
11
7
  import { makeModelRelation } from "./relation-runtime.js";
12
8
  import { DatabaseTestingTypeId } from "./testing.js";
13
9
  import {
@@ -15,6 +11,7 @@ import {
15
11
  releaseTestTransaction,
16
12
  releaseTransaction,
17
13
  type TransactionOrm,
14
+ withTransactionSemaphore,
18
15
  } from "./transaction.js";
19
16
 
20
17
  type OrmFor<Contract extends AnySqlContract> = ReturnType<typeof orm<Contract>>;
@@ -30,27 +27,62 @@ export type DefaultModels<Contract extends AnySqlContract> =
30
27
  ? Models
31
28
  : never;
32
29
 
33
- export interface DatabaseIdentifier<Contract extends AnySqlContract> {
34
- readonly _contract: Contract;
35
- readonly _databaseIdentifier: unique symbol;
30
+ type IsUnion<Value, Whole = Value> = Value extends unknown
31
+ ? [Whole] extends [Value]
32
+ ? false
33
+ : true
34
+ : never;
35
+
36
+ type IsSingletonString<Value extends string> =
37
+ Record<never, never> extends Record<Value, never> ? false : true;
38
+
39
+ const internalContextIdentifierPrefix = "\0@shivaedev/effect-prisma/internal/";
40
+ let activeTransactionReferenceSequence = 0;
41
+
42
+ export type DatabaseIdentifierLiteral<Identifier extends string> = [
43
+ Identifier,
44
+ ] extends [`\0@shivaedev/effect-prisma/internal/${string}`]
45
+ ? never
46
+ : string extends Identifier
47
+ ? never
48
+ : true extends IsUnion<Identifier>
49
+ ? never
50
+ : IsSingletonString<Identifier> extends true
51
+ ? Identifier
52
+ : never;
53
+
54
+ export interface DatabaseIdentifier<
55
+ Contract extends AnySqlContract,
56
+ Identifier extends string,
57
+ > {
58
+ readonly _contract: (contract: Contract) => Contract;
59
+ readonly _databaseIdentifier: (identifier: Identifier) => Identifier;
36
60
  }
37
61
 
38
- type DatabaseModels<Contract extends AnySqlContract, Requirement> = {
62
+ type DatabaseModels<
63
+ Contract extends AnySqlContract,
64
+ Identifier extends string,
65
+ > = {
39
66
  readonly [Model in keyof DefaultModels<Contract>]: Relation<
40
67
  DefaultModels<Contract>[Model],
41
- Requirement,
42
68
  Contract,
43
- Model & string
69
+ Model & string,
70
+ DatabaseIdentifier<Contract, Identifier>
44
71
  >;
45
72
  };
46
73
 
47
74
  export type DatabaseService<
48
75
  Contract extends AnySqlContract,
49
- Requirement,
50
- > = DatabaseModels<Contract, Requirement> & {
76
+ Identifier extends string,
77
+ > = DatabaseModels<Contract, Identifier> & {
51
78
  transaction<A, E, R>(
52
- program: Effect.Effect<A, E, R>,
53
- ): Effect.Effect<A, E | PrismaError, R | Requirement>;
79
+ program: Effect.Effect<A, E, R> &
80
+ (DatabaseIdentifier<Contract, Identifier> extends R ? unknown : never),
81
+ ): Effect.Effect<
82
+ A,
83
+ E | PrismaError,
84
+ Exclude<R, DatabaseIdentifier<Contract, Identifier>>
85
+ >;
54
86
  };
55
87
 
56
88
  /**
@@ -59,41 +91,27 @@ export type DatabaseService<
59
91
  */
60
92
  export interface DatabaseServiceHolder<
61
93
  Contract extends AnySqlContract,
62
- Requirement = ExecutorIdentifier<DefaultModels<Contract>>,
94
+ Identifier extends string,
63
95
  > extends Context.Service<
64
- DatabaseIdentifier<Contract>,
65
- DatabaseService<Contract, Requirement>
96
+ DatabaseIdentifier<Contract, Identifier>,
97
+ DatabaseService<Contract, Identifier>
66
98
  > {}
67
99
 
68
100
  export type AnyDatabase = Effect.Effect<unknown, never, unknown> & {
69
101
  readonly layer: (...arguments_: ReadonlyArray<never>) => Layer.Any;
70
102
  };
71
103
 
72
- /**
73
- * `Context.Service` is invariant in both of its parameters, so a holder built
74
- * for a concrete contract never matches `DatabaseServiceHolder<AnySqlContract,
75
- * …>`. The contract has to be inferred alongside the requirement, or the
76
- * conditional falls through to `never` for every real database.
77
- */
78
- export type DatabaseRequirement<Database> =
79
- Database extends DatabaseServiceHolder<
80
- infer _Contract extends AnySqlContract,
81
- infer Requirement
82
- >
83
- ? Requirement
84
- : never;
85
-
86
104
  export type DatabaseServiceOf<Database extends AnyDatabase> =
87
105
  Effect.Success<Database>;
88
106
 
89
- export interface SqlDatabase<Contract extends AnySqlContract, Options>
90
- extends DatabaseServiceHolder<Contract> {
107
+ export interface SqlDatabase<
108
+ Contract extends AnySqlContract,
109
+ Identifier extends string,
110
+ Options,
111
+ > extends DatabaseServiceHolder<Contract, Identifier> {
91
112
  readonly layer: (
92
113
  options: Options,
93
- ) => Layer.Layer<
94
- DatabaseIdentifier<Contract> | ExecutorIdentifier<DefaultModels<Contract>>,
95
- PrismaError
96
- >;
114
+ ) => Layer.Layer<DatabaseIdentifier<Contract, Identifier>, PrismaError>;
97
115
  }
98
116
 
99
117
  /**
@@ -121,68 +139,157 @@ export const namespaceModels = <
121
139
  };
122
140
 
123
141
  /**
124
- * Assemble the database service, its Executor service, and its Layer around a
125
- * driver-specific executor acquisition.
142
+ * Assemble the database service and its Layer around a driver-specific
143
+ * executor acquisition.
126
144
  */
127
- export const makeSqlDatabase = <const Contract extends AnySqlContract, Options>(
128
- identifier: string,
145
+ export const makeSqlDatabase = <
146
+ const Contract extends AnySqlContract,
147
+ const Identifier extends string,
148
+ Options,
149
+ >(
150
+ identifier: DatabaseIdentifierLiteral<Identifier>,
129
151
  acquireExecutor: (
130
152
  options: Options,
131
153
  ) => Effect.Effect<
132
154
  DatabaseExecutor<DefaultModels<Contract>, Contract>,
133
155
  PrismaError
134
156
  >,
135
- ): SqlDatabase<Contract, Options> => {
157
+ ): SqlDatabase<Contract, Identifier, Options> => {
158
+ if (identifier.startsWith(internalContextIdentifierPrefix)) {
159
+ throw new TypeError(
160
+ "Database identifier uses a reserved internal namespace",
161
+ );
162
+ }
136
163
  type Models = DefaultModels<Contract>;
137
- type ExecutorId = ExecutorIdentifier<Models>;
138
- type DatabaseId = DatabaseIdentifier<Contract>;
164
+ type DatabaseId = DatabaseIdentifier<Contract, Identifier>;
139
165
 
140
- const Executor = Context.Service<
141
- ExecutorId,
142
- DatabaseExecutor<Models, Contract>
143
- >(`${identifier}/Executor`);
144
166
  const Service = Context.Service<
145
167
  DatabaseId,
146
- DatabaseService<Contract, ExecutorId>
168
+ DatabaseService<Contract, Identifier>
147
169
  >(identifier);
170
+ const executors = new WeakMap<
171
+ DatabaseService<Contract, Identifier>,
172
+ DatabaseExecutor<Models, Contract>
173
+ >();
174
+ type ActiveTransaction = {
175
+ readonly executor: DatabaseExecutor<Models, Contract>;
176
+ readonly facade: DatabaseService<Contract, Identifier>;
177
+ };
178
+ const ActiveTransaction = Context.Reference<ActiveTransaction | undefined>(
179
+ `${internalContextIdentifierPrefix}ActiveTransaction/${activeTransactionReferenceSequence++}`,
180
+ { defaultValue: () => undefined },
181
+ );
182
+ const executorOf = (
183
+ facade: DatabaseService<Contract, Identifier>,
184
+ ): Effect.Effect<DatabaseExecutor<Models, Contract>> => {
185
+ const executor = executors.get(facade);
186
+ return executor === undefined
187
+ ? Effect.die(
188
+ new TypeError(
189
+ "The database service was not created by its database Layer",
190
+ ),
191
+ )
192
+ : Effect.succeed(executor);
193
+ };
194
+ const runTransaction = <A, E, R>(
195
+ current: DatabaseExecutor<Models, Contract>,
196
+ currentFacade: DatabaseService<Contract, Identifier>,
197
+ program: Effect.Effect<A, E, R>,
198
+ release: typeof releaseTransaction,
199
+ mode: "test" | "transaction",
200
+ span: string,
201
+ ): Effect.Effect<A, E | PrismaError, Exclude<R, DatabaseId>> => {
202
+ if (!current.liveness.open) {
203
+ return Effect.fail(toPrismaError({ code: current.liveness.closedCode }));
204
+ }
205
+ if (
206
+ current.mode === "test" ||
207
+ (current.mode === "transaction" && mode === "transaction")
208
+ ) {
209
+ return Effect.provideService(program, Service, currentFacade);
210
+ }
211
+ if (current.mode === "transaction") {
212
+ return Effect.fail(
213
+ toPrismaError({
214
+ code: "RUNTIME.TEST_TRANSACTION_INSIDE_TRANSACTION_UNSUPPORTED",
215
+ }),
216
+ );
217
+ }
148
218
 
149
- const scopedTransaction =
150
- (release: typeof releaseTransaction, span: string) =>
151
- <A, E, R>(
152
- program: Effect.Effect<A, E, R>,
153
- ): Effect.Effect<A, E | PrismaError, R | ExecutorId> =>
154
- Effect.flatMap(Executor, (current) => {
155
- if (current.transactional) {
156
- return program;
157
- }
158
-
159
- return Effect.acquireUseRelease(
160
- acquireTransaction(current, (transactionOrm) =>
161
- namespaceModels<Contract, Models>(
162
- current.client.contract,
163
- transactionOrm,
164
- ),
219
+ const transaction = Effect.acquireUseRelease(
220
+ acquireTransaction(
221
+ current,
222
+ (transactionOrm) =>
223
+ namespaceModels<Contract, Models>(
224
+ current.client.contract,
225
+ transactionOrm,
165
226
  ),
166
- (resource) =>
167
- program.pipe(Effect.provideService(Executor, resource.executor)),
168
- release,
169
- ).pipe(Effect.withSpan(span, { kind: "client" }));
170
- });
171
-
172
- const transaction = scopedTransaction(
173
- releaseTransaction,
174
- "prisma.transaction",
175
- );
176
- const withTestTransaction = scopedTransaction(
177
- releaseTestTransaction,
178
- "prisma.testTransaction",
179
- );
227
+ mode,
228
+ ),
229
+ (resource) => {
230
+ const transactionFacade = makeFacade(resource.executor);
231
+ return program.pipe(
232
+ Effect.provideService(Service, transactionFacade),
233
+ Effect.provideService(ActiveTransaction, {
234
+ executor: resource.executor,
235
+ facade: transactionFacade,
236
+ }),
237
+ );
238
+ },
239
+ release,
240
+ ).pipe(Effect.withSpan(span, { kind: "client" }));
241
+
242
+ return withTransactionSemaphore(current.transactionSemaphore, transaction);
243
+ };
180
244
 
181
- const facade = new Proxy(
182
- Object.assign(Object.create(null), {
183
- transaction,
184
- }) as DatabaseService<Contract, ExecutorId>,
185
- {
245
+ function makeFacade(
246
+ current: DatabaseExecutor<Models, Contract>,
247
+ ): DatabaseService<Contract, Identifier> {
248
+ let facade: DatabaseService<Contract, Identifier>;
249
+ const resolveActiveTransaction = Effect.flatMap(
250
+ ActiveTransaction,
251
+ (active) => {
252
+ if (current.mode === "root") {
253
+ return Effect.succeed(active ?? { executor: current, facade });
254
+ }
255
+ return active?.executor === current
256
+ ? Effect.succeed(active)
257
+ : Effect.fail(
258
+ toPrismaError({
259
+ code: "RUNTIME.TRANSACTION_CONTEXT_MISMATCH",
260
+ }),
261
+ );
262
+ },
263
+ );
264
+ const resolveExecutor = Effect.suspend(() =>
265
+ current.liveness.open
266
+ ? Effect.map(resolveActiveTransaction, ({ executor }) => executor)
267
+ : Effect.succeed(current),
268
+ );
269
+ const target = Object.assign(Object.create(null), {
270
+ transaction: <A, E, R>(
271
+ program: Effect.Effect<A, E, R> &
272
+ (DatabaseId extends R ? unknown : never),
273
+ ) =>
274
+ Effect.suspend(() => {
275
+ if (!current.liveness.open) {
276
+ return Effect.fail(
277
+ toPrismaError({ code: current.liveness.closedCode }),
278
+ );
279
+ }
280
+ return Effect.flatMap(resolveActiveTransaction, (selected) => {
281
+ return runTransaction(
282
+ selected.executor,
283
+ selected.facade,
284
+ program,
285
+ releaseTransaction,
286
+ "transaction",
287
+ "prisma.transaction",
288
+ );
289
+ });
290
+ }),
291
+ }) as DatabaseService<Contract, Identifier>;
292
+ facade = new Proxy(target, {
186
293
  get(target, property, receiver) {
187
294
  if (Reflect.has(target, property)) {
188
295
  return Reflect.get(target, property, receiver);
@@ -190,29 +297,45 @@ export const makeSqlDatabase = <const Contract extends AnySqlContract, Options>(
190
297
  if (typeof property !== "string") {
191
298
  return undefined;
192
299
  }
193
- return makeModelRelation(Executor, property);
300
+ return makeModelRelation(current, property, resolveExecutor);
194
301
  },
195
- },
196
- );
302
+ });
303
+ executors.set(facade, current);
304
+ return facade;
305
+ }
306
+
307
+ const withTestTransaction = <A, E, R>(
308
+ program: Effect.Effect<A, E, R> & (DatabaseId extends R ? unknown : never),
309
+ ): Effect.Effect<A, E | PrismaError, DatabaseId | Exclude<R, DatabaseId>> =>
310
+ Effect.flatMap(Service, (facade) => {
311
+ return Effect.flatMap(executorOf(facade), (current) =>
312
+ runTransaction(
313
+ current,
314
+ facade,
315
+ program,
316
+ releaseTestTransaction,
317
+ "test",
318
+ "prisma.testTransaction",
319
+ ),
320
+ );
321
+ });
197
322
 
198
323
  const layer = (
199
324
  layerOptions: Options,
200
- ): Layer.Layer<DatabaseId | ExecutorId, PrismaError> => {
325
+ ): Layer.Layer<DatabaseId, PrismaError> => {
201
326
  const acquire = Effect.acquireRelease(
202
327
  Effect.suspend(() => acquireExecutor(layerOptions)),
203
- (executor) =>
204
- Effect.promise(() => executor.client.close()).pipe(Effect.orDie),
328
+ (executor) => {
329
+ executor.liveness.open = false;
330
+ return Effect.promise(() => executor.client.close()).pipe(Effect.orDie);
331
+ },
205
332
  );
206
333
 
207
- return Layer.effectContext(
208
- Effect.map(acquire, (executor) =>
209
- Context.make(Executor, executor).pipe(Context.add(Service, facade)),
210
- ),
211
- );
334
+ return Layer.effect(Service)(Effect.map(acquire, makeFacade));
212
335
  };
213
336
 
214
337
  return Object.assign(Service, {
215
338
  layer,
216
339
  [DatabaseTestingTypeId]: { withTestTransaction },
217
- }) as SqlDatabase<Contract, Options>;
340
+ }) as SqlDatabase<Contract, Identifier, Options>;
218
341
  };
@@ -1,7 +1,7 @@
1
1
  import type { Contract as PrismaContract } from "@prisma-next/contract/types";
2
2
  import type { SqlStorage } from "@prisma-next/sql-contract/types";
3
3
  import type { ExecutionContext, Runtime } from "@prisma-next/sql-runtime";
4
- import type { Context, Semaphore } from "effect";
4
+ import type { Semaphore } from "effect";
5
5
 
6
6
  export type AnySqlContract = PrismaContract<SqlStorage>;
7
7
  export type AnyPostgresContract = AnySqlContract;
@@ -22,20 +22,14 @@ export interface DatabaseExecutor<
22
22
  Contract extends AnySqlContract = AnySqlContract,
23
23
  > {
24
24
  readonly client: SqlDatabaseClient<Contract>;
25
+ readonly identity: object;
26
+ readonly liveness: {
27
+ readonly closedCode: string;
28
+ open: boolean;
29
+ };
30
+ readonly mode: "root" | "test" | "transaction";
25
31
  readonly models: Models;
26
32
  readonly querySemaphore: Semaphore.Semaphore | undefined;
27
- readonly transactional: boolean;
28
- }
29
-
30
- export type ExecutorService<
31
- Models extends object,
32
- Contract extends AnySqlContract = AnySqlContract,
33
- > = Context.Service<
34
- ExecutorIdentifier<Models>,
35
- DatabaseExecutor<Models, Contract>
36
- >;
37
-
38
- export interface ExecutorIdentifier<Models extends object> {
39
- readonly _models: Models;
40
- readonly _executorIdentifier: unique symbol;
33
+ readonly transactionIdentity: object | undefined;
34
+ readonly transactionSemaphore: Semaphore.Semaphore | undefined;
41
35
  }
@@ -1,10 +1,18 @@
1
- import type { Semaphore } from "effect";
2
1
  import { Effect } from "effect";
2
+ import { type PrismaError, toPrismaError } from "../error.js";
3
+ import type { DatabaseExecutor } from "./executor.js";
3
4
 
4
5
  export const executeQuery = <A, E, R>(
5
- executor: { readonly querySemaphore: Semaphore.Semaphore | undefined },
6
+ executor: Pick<DatabaseExecutor<object>, "liveness" | "querySemaphore">,
6
7
  effect: Effect.Effect<A, E, R>,
7
- ): Effect.Effect<A, E, R> =>
8
- executor.querySemaphore === undefined
9
- ? effect
10
- : executor.querySemaphore.withPermit(Effect.uninterruptible(effect));
8
+ ): Effect.Effect<A, E | PrismaError, R> => {
9
+ const checked = Effect.suspend(
10
+ (): Effect.Effect<A, E | PrismaError, R> =>
11
+ executor.liveness.open
12
+ ? effect
13
+ : Effect.fail(toPrismaError({ code: executor.liveness.closedCode })),
14
+ );
15
+ return executor.querySemaphore === undefined
16
+ ? checked
17
+ : executor.querySemaphore.withPermit(Effect.uninterruptible(checked));
18
+ };
@@ -62,7 +62,24 @@ const applyMethod = (
62
62
  return Reflect.apply(method, current, arguments_);
63
63
  };
64
64
 
65
- const replayPlan = (collection: unknown, plan: RelationPlan): unknown => {
65
+ const replayPlan = (
66
+ collection: unknown,
67
+ plan: RelationPlan,
68
+ owner: object,
69
+ transactionIdentity: object | undefined,
70
+ ): unknown => {
71
+ if (!plan.liveness.open) {
72
+ throw new TypeError("Included Relation is closed");
73
+ }
74
+ if (plan.owner !== owner) {
75
+ throw new TypeError("Included Relations must use the same Database");
76
+ }
77
+ if (
78
+ plan.transactionIdentity !== undefined &&
79
+ plan.transactionIdentity !== transactionIdentity
80
+ ) {
81
+ throw new TypeError("Included Relation belongs to another transaction");
82
+ }
66
83
  const relatedModel =
67
84
  typeof collection === "object" && collection !== null
68
85
  ? Reflect.get(collection, "modelName")
@@ -73,7 +90,12 @@ const replayPlan = (collection: unknown, plan: RelationPlan): unknown => {
73
90
  );
74
91
  }
75
92
 
76
- const refined = replayRecipeFrom(collection, plan.recipe);
93
+ const refined = replayRecipeFrom(
94
+ collection,
95
+ plan.recipe,
96
+ owner,
97
+ transactionIdentity,
98
+ );
77
99
  if (plan.terminal !== "count") {
78
100
  return refined;
79
101
  }
@@ -82,10 +104,13 @@ const replayPlan = (collection: unknown, plan: RelationPlan): unknown => {
82
104
 
83
105
  const includeRefinement = (
84
106
  value: unknown,
107
+ owner: object,
108
+ transactionIdentity: object | undefined,
85
109
  ): ((collection: unknown) => unknown) => {
86
110
  const plan = getRelationPlan(value);
87
111
  if (plan !== undefined) {
88
- return (collection) => replayPlan(collection, plan);
112
+ return (collection) =>
113
+ replayPlan(collection, plan, owner, transactionIdentity);
89
114
  }
90
115
 
91
116
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -115,19 +140,31 @@ const includeRefinement = (
115
140
  const branches = Object.fromEntries(
116
141
  plans.map(([name, queryPlan]) => [
117
142
  name,
118
- replayPlan(collection, queryPlan),
143
+ replayPlan(collection, queryPlan, owner, transactionIdentity),
119
144
  ]),
120
145
  );
121
146
  return applyMethod(collection, "combine", [branches], model ?? "relation");
122
147
  };
123
148
  };
124
149
 
125
- const replayRecipeFrom = (root: unknown, recipe: RelationRecipe): unknown => {
150
+ const replayRecipeFrom = (
151
+ root: unknown,
152
+ recipe: RelationRecipe,
153
+ owner: object,
154
+ transactionIdentity: object | undefined,
155
+ ): unknown => {
126
156
  let current = root;
127
157
  for (const operation of operations(recipe)) {
128
158
  const arguments_ =
129
159
  operation.name === "include" && operation.arguments.length === 2
130
- ? [operation.arguments[0], includeRefinement(operation.arguments[1])]
160
+ ? [
161
+ operation.arguments[0],
162
+ includeRefinement(
163
+ operation.arguments[1],
164
+ owner,
165
+ transactionIdentity,
166
+ ),
167
+ ]
131
168
  : operation.arguments;
132
169
  current = applyMethod(current, operation.name, arguments_, recipe.model);
133
170
  }
@@ -137,6 +174,8 @@ const replayRecipeFrom = (root: unknown, recipe: RelationRecipe): unknown => {
137
174
  export const replayRecipe = (
138
175
  models: object,
139
176
  recipe: RelationRecipe,
177
+ owner: object,
178
+ transactionIdentity: object | undefined,
140
179
  ): unknown => {
141
180
  const current: unknown = Reflect.get(models, recipe.model);
142
181
 
@@ -144,5 +183,5 @@ export const replayRecipe = (
144
183
  throw new TypeError(`Unknown Prisma model: ${recipe.model}`);
145
184
  }
146
185
 
147
- return replayRecipeFrom(current, recipe);
186
+ return replayRecipeFrom(current, recipe, owner, transactionIdentity);
148
187
  };
@@ -1,22 +1,22 @@
1
1
  import type { RelationRecipe } from "./recipe.js";
2
2
 
3
- export const RelationPlanTypeId = Symbol.for(
4
- "@shivaedev/effect-prisma/RelationPlan",
5
- );
6
-
7
3
  export interface RelationPlan {
4
+ readonly liveness: { readonly open: boolean };
5
+ readonly owner: object;
8
6
  readonly recipe: RelationRecipe;
9
7
  readonly terminal?: PropertyKey;
8
+ readonly transactionIdentity: object | undefined;
10
9
  }
11
10
 
11
+ const plans = new WeakMap<object, RelationPlan>();
12
+
13
+ export const setRelationPlan = (value: object, plan: RelationPlan): void => {
14
+ plans.set(value, plan);
15
+ };
16
+
12
17
  export const getRelationPlan = (value: unknown): RelationPlan | undefined => {
13
- if (
14
- typeof value !== "object" ||
15
- value === null ||
16
- !Reflect.has(value, RelationPlanTypeId)
17
- ) {
18
+ if (typeof value !== "object" || value === null) {
18
19
  return undefined;
19
20
  }
20
-
21
- return Reflect.get(value, RelationPlanTypeId) as RelationPlan;
21
+ return plans.get(value);
22
22
  };