@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.
- package/CHANGELOG.md +18 -0
- package/README.md +25 -7
- package/dist/database.d.ts +6 -6
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +9 -2
- package/dist/database.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/database-factory.d.ts +20 -22
- package/dist/internal/database-factory.d.ts.map +1 -1
- package/dist/internal/database-factory.js +83 -24
- package/dist/internal/database-factory.js.map +1 -1
- package/dist/internal/executor.d.ts +9 -7
- package/dist/internal/executor.d.ts.map +1 -1
- package/dist/internal/query-execution.d.ts +3 -4
- package/dist/internal/query-execution.d.ts.map +1 -1
- package/dist/internal/query-execution.js +9 -3
- package/dist/internal/query-execution.js.map +1 -1
- package/dist/internal/recipe.d.ts +1 -1
- package/dist/internal/recipe.d.ts.map +1 -1
- package/dist/internal/recipe.js +22 -9
- package/dist/internal/recipe.js.map +1 -1
- package/dist/internal/relation-plan.d.ts +6 -1
- package/dist/internal/relation-plan.d.ts.map +1 -1
- package/dist/internal/relation-plan.js +6 -5
- package/dist/internal/relation-plan.js.map +1 -1
- package/dist/internal/relation-runtime.d.ts +4 -3
- package/dist/internal/relation-runtime.d.ts.map +1 -1
- package/dist/internal/relation-runtime.js +38 -19
- package/dist/internal/relation-runtime.js.map +1 -1
- package/dist/internal/relation-stream.d.ts +3 -4
- package/dist/internal/relation-stream.d.ts.map +1 -1
- package/dist/internal/relation-stream.js +23 -21
- package/dist/internal/relation-stream.js.map +1 -1
- package/dist/internal/testing.d.ts +5 -5
- package/dist/internal/testing.d.ts.map +1 -1
- package/dist/internal/testing.js.map +1 -1
- package/dist/internal/transaction.d.ts +6 -3
- package/dist/internal/transaction.d.ts.map +1 -1
- package/dist/internal/transaction.js +14 -4
- package/dist/internal/transaction.js.map +1 -1
- package/dist/internal/vitest-database.d.ts +2 -2
- package/dist/internal/vitest-database.d.ts.map +1 -1
- package/dist/internal/vitest-database.js.map +1 -1
- package/dist/relation/include.d.ts +10 -8
- package/dist/relation/include.d.ts.map +1 -1
- package/dist/relation/prisma-methods.d.ts +20 -20
- package/dist/relation/prisma-methods.d.ts.map +1 -1
- package/dist/relation.d.ts +8 -7
- package/dist/relation.d.ts.map +1 -1
- package/dist/sqlite.d.ts +6 -6
- package/dist/sqlite.d.ts.map +1 -1
- package/dist/sqlite.js +10 -2
- package/dist/sqlite.js.map +1 -1
- package/dist/testing/transaction.d.ts +3 -3
- package/dist/testing/transaction.d.ts.map +1 -1
- package/dist/testing/transaction.js.map +1 -1
- package/dist/testing/types.d.ts +1 -1
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/vitest.d.ts +3 -3
- package/dist/testing/vitest.d.ts.map +1 -1
- package/dist/testing/vitest.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/database.ts +53 -47
- package/src/index.ts +0 -1
- package/src/internal/database-factory.ts +221 -98
- package/src/internal/executor.ts +9 -15
- package/src/internal/query-execution.ts +14 -6
- package/src/internal/recipe.ts +46 -7
- package/src/internal/relation-plan.ts +11 -11
- package/src/internal/relation-runtime.ts +67 -65
- package/src/internal/relation-stream.ts +54 -48
- package/src/internal/testing.ts +8 -8
- package/src/internal/transaction.ts +80 -56
- package/src/internal/vitest-database.ts +3 -12
- package/src/relation/include.ts +31 -22
- package/src/relation/prisma-methods.ts +27 -31
- package/src/relation.ts +19 -27
- package/src/sqlite.ts +61 -54
- package/src/testing/transaction.ts +16 -6
- package/src/testing/types.ts +1 -1
- package/src/testing/vitest.ts +3 -10
- 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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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<
|
|
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
|
-
|
|
50
|
-
> = DatabaseModels<Contract,
|
|
76
|
+
Identifier extends string,
|
|
77
|
+
> = DatabaseModels<Contract, Identifier> & {
|
|
51
78
|
transaction<A, E, R>(
|
|
52
|
-
program: Effect.Effect<A, E, R
|
|
53
|
-
|
|
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
|
-
|
|
94
|
+
Identifier extends string,
|
|
63
95
|
> extends Context.Service<
|
|
64
|
-
DatabaseIdentifier<Contract>,
|
|
65
|
-
DatabaseService<Contract,
|
|
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<
|
|
90
|
-
extends
|
|
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
|
|
125
|
-
*
|
|
142
|
+
* Assemble the database service and its Layer around a driver-specific
|
|
143
|
+
* executor acquisition.
|
|
126
144
|
*/
|
|
127
|
-
export const makeSqlDatabase = <
|
|
128
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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(
|
|
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
|
|
325
|
+
): Layer.Layer<DatabaseId, PrismaError> => {
|
|
201
326
|
const acquire = Effect.acquireRelease(
|
|
202
327
|
Effect.suspend(() => acquireExecutor(layerOptions)),
|
|
203
|
-
(executor) =>
|
|
204
|
-
|
|
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.
|
|
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
|
};
|
package/src/internal/executor.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
6
|
+
executor: Pick<DatabaseExecutor<object>, "liveness" | "querySemaphore">,
|
|
6
7
|
effect: Effect.Effect<A, E, R>,
|
|
7
|
-
): Effect.Effect<A, E, R> =>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
};
|
package/src/internal/recipe.ts
CHANGED
|
@@ -62,7 +62,24 @@ const applyMethod = (
|
|
|
62
62
|
return Reflect.apply(method, current, arguments_);
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
const replayPlan = (
|
|
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(
|
|
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) =>
|
|
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 = (
|
|
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
|
-
? [
|
|
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
|
};
|