@shivaedev/effect-prisma 0.6.2 → 0.6.4
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 +13 -1
- package/dist/database.d.ts +4 -4
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +12 -12
- package/dist/database.js.map +1 -1
- package/dist/error.d.ts +5 -5
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +1 -2
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/internal/adapters/transaction-settlement.d.ts +10 -0
- package/dist/internal/adapters/transaction-settlement.d.ts.map +1 -0
- package/dist/internal/adapters/transaction-settlement.js +56 -0
- package/dist/internal/adapters/transaction-settlement.js.map +1 -0
- package/dist/internal/client-lifecycle.d.ts.map +1 -1
- package/dist/internal/client-lifecycle.js.map +1 -1
- package/dist/internal/contract-normalization.d.ts.map +1 -1
- package/dist/internal/contract-normalization.js +1 -3
- package/dist/internal/contract-normalization.js.map +1 -1
- package/dist/internal/database-facade.d.ts +24 -0
- package/dist/internal/database-facade.d.ts.map +1 -0
- package/dist/internal/database-facade.js +84 -0
- package/dist/internal/database-facade.js.map +1 -0
- package/dist/internal/database-factory.d.ts +4 -44
- package/dist/internal/database-factory.d.ts.map +1 -1
- package/dist/internal/database-factory.js +11 -84
- package/dist/internal/database-factory.js.map +1 -1
- package/dist/internal/database-types.d.ts +37 -0
- package/dist/internal/database-types.d.ts.map +1 -0
- package/dist/internal/database-types.js +2 -0
- package/dist/internal/database-types.js.map +1 -0
- package/dist/internal/dynamic.d.ts +5 -0
- package/dist/internal/dynamic.d.ts.map +1 -0
- package/dist/internal/dynamic.js +12 -0
- package/dist/internal/dynamic.js.map +1 -0
- package/dist/internal/executor.d.ts.map +1 -1
- package/dist/internal/promise.d.ts +1 -1
- package/dist/internal/promise.d.ts.map +1 -1
- package/dist/internal/promise.js +1 -3
- package/dist/internal/promise.js.map +1 -1
- package/dist/internal/query-execution.d.ts +2 -2
- package/dist/internal/query-execution.d.ts.map +1 -1
- package/dist/internal/query-execution.js +2 -6
- package/dist/internal/query-execution.js.map +1 -1
- package/dist/internal/recipe.d.ts.map +1 -1
- package/dist/internal/recipe.js +7 -18
- package/dist/internal/recipe.js.map +1 -1
- package/dist/internal/relation-plan.d.ts +1 -1
- package/dist/internal/relation-result.d.ts +4 -0
- package/dist/internal/relation-result.d.ts.map +1 -0
- package/dist/internal/relation-result.js +19 -0
- package/dist/internal/relation-result.js.map +1 -0
- package/dist/internal/relation-runtime.d.ts +3 -3
- package/dist/internal/relation-runtime.d.ts.map +1 -1
- package/dist/internal/relation-runtime.js +9 -34
- package/dist/internal/relation-runtime.js.map +1 -1
- package/dist/internal/relation-stream.d.ts +3 -3
- package/dist/internal/relation-stream.d.ts.map +1 -1
- package/dist/internal/relation-stream.js +12 -14
- package/dist/internal/relation-stream.js.map +1 -1
- package/dist/internal/sqlite-datetime.d.ts +1 -1
- package/dist/internal/sqlite-datetime.d.ts.map +1 -1
- package/dist/internal/sqlite-datetime.js +3 -1
- package/dist/internal/sqlite-datetime.js.map +1 -1
- package/dist/internal/sqlite-pragmas.d.ts.map +1 -1
- package/dist/internal/sqlite-pragmas.js.map +1 -1
- package/dist/internal/testing.d.ts +1 -1
- package/dist/internal/testing.d.ts.map +1 -1
- package/dist/internal/testing.js +5 -2
- package/dist/internal/testing.js.map +1 -1
- package/dist/internal/transaction.d.ts +5 -5
- package/dist/internal/transaction.d.ts.map +1 -1
- package/dist/internal/transaction.js +3 -51
- package/dist/internal/transaction.js.map +1 -1
- package/dist/relation/include-metadata.d.ts.map +1 -1
- package/dist/relation/include.d.ts +2 -2
- package/dist/relation/include.d.ts.map +1 -1
- package/dist/relation/prisma-methods.d.ts +3 -3
- package/dist/relation/prisma-methods.d.ts.map +1 -1
- package/dist/relation.d.ts +2 -2
- package/dist/relation.d.ts.map +1 -1
- package/dist/sqlite.d.ts +3 -3
- package/dist/sqlite.d.ts.map +1 -1
- package/dist/sqlite.js +14 -12
- package/dist/sqlite.js.map +1 -1
- package/dist/testing/transaction.d.ts +6 -7
- package/dist/testing/transaction.d.ts.map +1 -1
- package/dist/testing/transaction.js +2 -2
- package/dist/testing/transaction.js.map +1 -1
- package/dist/testing/types.d.ts +2 -2
- 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 +2 -2
- package/dist/testing/vitest.js.map +1 -1
- package/dist/testing.d.ts +3 -3
- package/package.json +6 -6
- package/src/bin/normalize-contract.ts +1 -1
- package/src/database.ts +49 -67
- package/src/error.ts +9 -19
- package/src/index.ts +3 -3
- package/src/internal/adapters/transaction-settlement.ts +67 -0
- package/src/internal/client-lifecycle.ts +3 -10
- package/src/internal/contract-normalization.ts +3 -8
- package/src/internal/database-facade.ts +143 -0
- package/src/internal/database-factory.ts +30 -298
- package/src/internal/database-types.ts +69 -0
- package/src/internal/dynamic.ts +16 -0
- package/src/internal/executor.ts +1 -4
- package/src/internal/promise.ts +3 -11
- package/src/internal/query-execution.ts +4 -8
- package/src/internal/recipe.ts +19 -84
- package/src/internal/relation-plan.ts +1 -1
- package/src/internal/relation-result.ts +27 -0
- package/src/internal/relation-runtime.ts +43 -133
- package/src/internal/relation-stream.ts +22 -48
- package/src/internal/sqlite-datetime.ts +10 -23
- package/src/internal/sqlite-pragmas.ts +1 -4
- package/src/internal/testing.ts +13 -15
- package/src/internal/transaction.ts +14 -107
- package/src/relation/include-metadata.ts +10 -42
- package/src/relation/include.ts +19 -115
- package/src/relation/prisma-methods.ts +23 -111
- package/src/relation.ts +18 -46
- package/src/sqlite.ts +57 -78
- package/src/testing/transaction.ts +20 -33
- package/src/testing/types.ts +9 -28
- package/src/testing/vitest.ts +14 -28
- package/src/testing.ts +3 -3
|
@@ -1,328 +1,60 @@
|
|
|
1
|
-
import type { orm } from "@prisma-next/sql-orm-client";
|
|
2
1
|
import { Context, Effect, Layer } from "effect";
|
|
3
|
-
import {
|
|
4
|
-
import type
|
|
5
|
-
import { assertAvailableModelNames } from "./client-lifecycle.js";
|
|
6
|
-
import type { AnySqlContract, DatabaseExecutor } from "./executor.js";
|
|
7
|
-
import { makeModelRelation } from "./relation-runtime.js";
|
|
8
|
-
import { DatabaseTestingTypeId } from "./testing.js";
|
|
2
|
+
import type { PrismaError } from "../error.ts";
|
|
3
|
+
import { type ActiveTransaction, type FacadeScope, makeFacade, runTransaction } from "./database-facade.ts";
|
|
9
4
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
type DatabaseIdentifier,
|
|
6
|
+
type DatabaseIdentifierLiteral,
|
|
7
|
+
type DatabaseService,
|
|
8
|
+
type DefaultModels,
|
|
9
|
+
internalContextIdentifierPrefix,
|
|
10
|
+
type SqlDatabase,
|
|
11
|
+
} from "./database-types.ts";
|
|
12
|
+
import type { AnySqlContract, DatabaseExecutor } from "./executor.ts";
|
|
13
|
+
import { DatabaseTestingTypeId } from "./testing.ts";
|
|
14
|
+
import { releaseTestTransaction } from "./transaction.ts";
|
|
16
15
|
|
|
17
|
-
type OrmFor<Contract extends AnySqlContract> = ReturnType<typeof orm<Contract>>;
|
|
18
|
-
type DefaultNamespaceId<Contract extends AnySqlContract> =
|
|
19
|
-
"__unbound__" extends keyof OrmFor<Contract>
|
|
20
|
-
? "__unbound__"
|
|
21
|
-
: "public" extends keyof OrmFor<Contract>
|
|
22
|
-
? "public"
|
|
23
|
-
: keyof OrmFor<Contract>;
|
|
24
|
-
export type DefaultModels<Contract extends AnySqlContract> =
|
|
25
|
-
OrmFor<Contract>[DefaultNamespaceId<Contract>] extends infer Models extends
|
|
26
|
-
object
|
|
27
|
-
? Models
|
|
28
|
-
: never;
|
|
29
|
-
|
|
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
16
|
let activeTransactionReferenceSequence = 0;
|
|
41
17
|
|
|
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;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type DatabaseModels<
|
|
63
|
-
Contract extends AnySqlContract,
|
|
64
|
-
Identifier extends string,
|
|
65
|
-
> = {
|
|
66
|
-
readonly [Model in keyof DefaultModels<Contract>]: Relation<
|
|
67
|
-
DefaultModels<Contract>[Model],
|
|
68
|
-
Contract,
|
|
69
|
-
Model & string,
|
|
70
|
-
DatabaseIdentifier<Contract, Identifier>
|
|
71
|
-
>;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export type DatabaseService<
|
|
75
|
-
Contract extends AnySqlContract,
|
|
76
|
-
Identifier extends string,
|
|
77
|
-
> = DatabaseModels<Contract, Identifier> & {
|
|
78
|
-
transaction<A, E, R>(
|
|
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
|
-
>;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* The driver-independent part of a database definition: the Context service
|
|
90
|
-
* carrying the typed facade. Driver entrypoints add their own `layer` options.
|
|
91
|
-
*/
|
|
92
|
-
export interface DatabaseServiceHolder<
|
|
93
|
-
Contract extends AnySqlContract,
|
|
94
|
-
Identifier extends string,
|
|
95
|
-
> extends Context.Service<
|
|
96
|
-
DatabaseIdentifier<Contract, Identifier>,
|
|
97
|
-
DatabaseService<Contract, Identifier>
|
|
98
|
-
> {}
|
|
99
|
-
|
|
100
|
-
export type AnyDatabase = Effect.Effect<unknown, never, unknown> & {
|
|
101
|
-
readonly layer: (...arguments_: ReadonlyArray<never>) => Layer.Any;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export type DatabaseServiceOf<Database extends AnyDatabase> =
|
|
105
|
-
Effect.Success<Database>;
|
|
106
|
-
|
|
107
|
-
export interface SqlDatabase<
|
|
108
|
-
Contract extends AnySqlContract,
|
|
109
|
-
Identifier extends string,
|
|
110
|
-
Options,
|
|
111
|
-
> extends DatabaseServiceHolder<Contract, Identifier> {
|
|
112
|
-
readonly layer: (
|
|
113
|
-
options: Options,
|
|
114
|
-
) => Layer.Layer<DatabaseIdentifier<Contract, Identifier>, PrismaError>;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Resolve the models of the single domain namespace from a namespaced ORM
|
|
119
|
-
* client. Prisma Next namespaces PostgreSQL models under their schema and
|
|
120
|
-
* SQLite models under the unbound namespace.
|
|
121
|
-
*/
|
|
122
|
-
export const namespaceModels = <
|
|
123
|
-
Contract extends AnySqlContract,
|
|
124
|
-
Models extends object,
|
|
125
|
-
>(
|
|
126
|
-
contract: Contract,
|
|
127
|
-
namespacedOrm: TransactionOrm<Contract>,
|
|
128
|
-
): Models => {
|
|
129
|
-
const namespaces = Object.keys(contract.domain.namespaces);
|
|
130
|
-
if (namespaces.length !== 1 || namespaces[0] === undefined) {
|
|
131
|
-
throw new TypeError(
|
|
132
|
-
"Effect Prisma currently requires exactly one domain namespace",
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
assertAvailableModelNames(
|
|
136
|
-
Object.keys(contract.domain.namespaces[namespaces[0]].models),
|
|
137
|
-
);
|
|
138
|
-
return Reflect.get(namespacedOrm, namespaces[0]) as Models;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
18
|
/**
|
|
142
19
|
* Assemble the database service and its Layer around a driver-specific
|
|
143
20
|
* executor acquisition.
|
|
144
21
|
*/
|
|
145
|
-
export const makeSqlDatabase = <
|
|
146
|
-
const Contract extends AnySqlContract,
|
|
147
|
-
const Identifier extends string,
|
|
148
|
-
Options,
|
|
149
|
-
>(
|
|
22
|
+
export const makeSqlDatabase = <const Contract extends AnySqlContract, const Identifier extends string, Options>(
|
|
150
23
|
identifier: DatabaseIdentifierLiteral<Identifier>,
|
|
151
|
-
acquireExecutor: (
|
|
152
|
-
options: Options,
|
|
153
|
-
) => Effect.Effect<
|
|
154
|
-
DatabaseExecutor<DefaultModels<Contract>, Contract>,
|
|
155
|
-
PrismaError
|
|
156
|
-
>,
|
|
24
|
+
acquireExecutor: (options: Options) => Effect.Effect<DatabaseExecutor<DefaultModels<Contract>, Contract>, PrismaError>,
|
|
157
25
|
): SqlDatabase<Contract, Identifier, Options> => {
|
|
158
26
|
if (identifier.startsWith(internalContextIdentifierPrefix)) {
|
|
159
|
-
throw new TypeError(
|
|
160
|
-
"Database identifier uses a reserved internal namespace",
|
|
161
|
-
);
|
|
27
|
+
throw new TypeError("Database identifier uses a reserved internal namespace");
|
|
162
28
|
}
|
|
163
29
|
type Models = DefaultModels<Contract>;
|
|
164
30
|
type DatabaseId = DatabaseIdentifier<Contract, Identifier>;
|
|
165
31
|
|
|
166
|
-
const Service = Context.Service<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
type ActiveTransaction = {
|
|
175
|
-
readonly executor: DatabaseExecutor<Models, Contract>;
|
|
176
|
-
readonly facade: DatabaseService<Contract, Identifier>;
|
|
32
|
+
const Service = Context.Service<DatabaseId, DatabaseService<Contract, Identifier>>(identifier);
|
|
33
|
+
const scope: FacadeScope<Contract, Identifier> = {
|
|
34
|
+
activeTransaction: Context.Reference<ActiveTransaction<Contract, Identifier> | undefined>(
|
|
35
|
+
`${internalContextIdentifierPrefix}ActiveTransaction/${activeTransactionReferenceSequence++}`,
|
|
36
|
+
{ defaultValue: () => undefined },
|
|
37
|
+
),
|
|
38
|
+
executors: new WeakMap(),
|
|
39
|
+
service: Service,
|
|
177
40
|
};
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
{ defaultValue: () => undefined },
|
|
181
|
-
);
|
|
182
|
-
const executorOf = (
|
|
183
|
-
facade: DatabaseService<Contract, Identifier>,
|
|
184
|
-
): Effect.Effect<DatabaseExecutor<Models, Contract>> => {
|
|
185
|
-
const executor = executors.get(facade);
|
|
41
|
+
const executorOf = (facade: DatabaseService<Contract, Identifier>): Effect.Effect<DatabaseExecutor<Models, Contract>> => {
|
|
42
|
+
const executor = scope.executors.get(facade);
|
|
186
43
|
return executor === undefined
|
|
187
|
-
? Effect.die(
|
|
188
|
-
new TypeError(
|
|
189
|
-
"The database service was not created by its database Layer",
|
|
190
|
-
),
|
|
191
|
-
)
|
|
44
|
+
? Effect.die(new TypeError("The database service was not created by its database Layer"))
|
|
192
45
|
: Effect.succeed(executor);
|
|
193
46
|
};
|
|
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
|
-
}
|
|
218
|
-
|
|
219
|
-
const transaction = Effect.acquireUseRelease(
|
|
220
|
-
acquireTransaction(
|
|
221
|
-
current,
|
|
222
|
-
(transactionOrm) =>
|
|
223
|
-
namespaceModels<Contract, Models>(
|
|
224
|
-
current.client.contract,
|
|
225
|
-
transactionOrm,
|
|
226
|
-
),
|
|
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
|
-
};
|
|
244
|
-
|
|
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, {
|
|
293
|
-
get(target, property, receiver) {
|
|
294
|
-
if (Reflect.has(target, property)) {
|
|
295
|
-
return Reflect.get(target, property, receiver);
|
|
296
|
-
}
|
|
297
|
-
if (typeof property !== "string") {
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
return makeModelRelation(current, property, resolveExecutor);
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
executors.set(facade, current);
|
|
304
|
-
return facade;
|
|
305
|
-
}
|
|
306
47
|
|
|
307
48
|
const withTestTransaction = <A, E, R>(
|
|
308
49
|
program: Effect.Effect<A, E, R> & (DatabaseId extends R ? unknown : never),
|
|
309
50
|
): Effect.Effect<A, E | PrismaError, DatabaseId | Exclude<R, DatabaseId>> =>
|
|
310
51
|
Effect.flatMap(Service, (facade) => {
|
|
311
52
|
return Effect.flatMap(executorOf(facade), (current) =>
|
|
312
|
-
runTransaction(
|
|
313
|
-
current,
|
|
314
|
-
facade,
|
|
315
|
-
program,
|
|
316
|
-
releaseTestTransaction,
|
|
317
|
-
"test",
|
|
318
|
-
"prisma.testTransaction",
|
|
319
|
-
),
|
|
53
|
+
runTransaction(scope, current, facade, program, releaseTestTransaction, "test", "prisma.testTransaction"),
|
|
320
54
|
);
|
|
321
55
|
});
|
|
322
56
|
|
|
323
|
-
const layer = (
|
|
324
|
-
layerOptions: Options,
|
|
325
|
-
): Layer.Layer<DatabaseId, PrismaError> => {
|
|
57
|
+
const layer = (layerOptions: Options): Layer.Layer<DatabaseId, PrismaError> => {
|
|
326
58
|
const acquire = Effect.acquireRelease(
|
|
327
59
|
Effect.suspend(() => acquireExecutor(layerOptions)),
|
|
328
60
|
(executor) => {
|
|
@@ -331,11 +63,11 @@ export const makeSqlDatabase = <
|
|
|
331
63
|
},
|
|
332
64
|
);
|
|
333
65
|
|
|
334
|
-
return Layer.effect(Service)(Effect.map(acquire, makeFacade));
|
|
66
|
+
return Layer.effect(Service)(Effect.map(acquire, (executor) => makeFacade(scope, executor)));
|
|
335
67
|
};
|
|
336
68
|
|
|
337
69
|
return Object.assign(Service, {
|
|
338
70
|
layer,
|
|
339
71
|
[DatabaseTestingTypeId]: { withTestTransaction },
|
|
340
|
-
})
|
|
72
|
+
});
|
|
341
73
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { orm } from "@prisma-next/sql-orm-client";
|
|
2
|
+
import type { Context, Effect, Layer } from "effect";
|
|
3
|
+
import type { PrismaError } from "../error.ts";
|
|
4
|
+
import type { Relation } from "../relation.ts";
|
|
5
|
+
import type { AnySqlContract } from "./executor.ts";
|
|
6
|
+
|
|
7
|
+
type OrmFor<Contract extends AnySqlContract> = ReturnType<typeof orm<Contract>>;
|
|
8
|
+
type DefaultNamespaceId<Contract extends AnySqlContract> = "__unbound__" extends keyof OrmFor<Contract>
|
|
9
|
+
? "__unbound__"
|
|
10
|
+
: "public" extends keyof OrmFor<Contract>
|
|
11
|
+
? "public"
|
|
12
|
+
: keyof OrmFor<Contract>;
|
|
13
|
+
export type DefaultModels<Contract extends AnySqlContract> = OrmFor<Contract>[DefaultNamespaceId<Contract>] extends infer Models extends object
|
|
14
|
+
? Models
|
|
15
|
+
: never;
|
|
16
|
+
|
|
17
|
+
type IsUnion<Value, Whole = Value> = Value extends unknown ? ([Whole] extends [Value] ? false : true) : never;
|
|
18
|
+
|
|
19
|
+
type IsSingletonString<Value extends string> = Record<never, never> extends Record<Value, never> ? false : true;
|
|
20
|
+
|
|
21
|
+
export const internalContextIdentifierPrefix = "\0@shivaedev/effect-prisma/internal/";
|
|
22
|
+
|
|
23
|
+
export type DatabaseIdentifierLiteral<Identifier extends string> = [Identifier] extends [`\0@shivaedev/effect-prisma/internal/${string}`]
|
|
24
|
+
? never
|
|
25
|
+
: string extends Identifier
|
|
26
|
+
? never
|
|
27
|
+
: true extends IsUnion<Identifier>
|
|
28
|
+
? never
|
|
29
|
+
: IsSingletonString<Identifier> extends true
|
|
30
|
+
? Identifier
|
|
31
|
+
: never;
|
|
32
|
+
|
|
33
|
+
export interface DatabaseIdentifier<Contract extends AnySqlContract, Identifier extends string> {
|
|
34
|
+
readonly _contract: (contract: Contract) => Contract;
|
|
35
|
+
readonly _databaseIdentifier: (identifier: Identifier) => Identifier;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type DatabaseModels<Contract extends AnySqlContract, Identifier extends string> = {
|
|
39
|
+
readonly [Model in keyof DefaultModels<Contract>]: Relation<
|
|
40
|
+
DefaultModels<Contract>[Model],
|
|
41
|
+
Contract,
|
|
42
|
+
Model & string,
|
|
43
|
+
DatabaseIdentifier<Contract, Identifier>
|
|
44
|
+
>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type DatabaseService<Contract extends AnySqlContract, Identifier extends string> = DatabaseModels<Contract, Identifier> & {
|
|
48
|
+
transaction<A, E, R>(
|
|
49
|
+
program: Effect.Effect<A, E, R> & (DatabaseIdentifier<Contract, Identifier> extends R ? unknown : never),
|
|
50
|
+
): Effect.Effect<A, E | PrismaError, Exclude<R, DatabaseIdentifier<Contract, Identifier>>>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The driver-independent part of a database definition: the Context service
|
|
55
|
+
* carrying the typed facade. Driver entrypoints add their own `layer` options.
|
|
56
|
+
*/
|
|
57
|
+
export interface DatabaseServiceHolder<Contract extends AnySqlContract, Identifier extends string>
|
|
58
|
+
extends Context.Service<DatabaseIdentifier<Contract, Identifier>, DatabaseService<Contract, Identifier>> {}
|
|
59
|
+
|
|
60
|
+
export type AnyDatabase = Effect.Effect<unknown, never, unknown> & {
|
|
61
|
+
readonly layer: (...arguments_: ReadonlyArray<never>) => Layer.Any;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type DatabaseServiceOf<Database extends AnyDatabase> = Effect.Success<Database>;
|
|
65
|
+
|
|
66
|
+
export interface SqlDatabase<Contract extends AnySqlContract, Identifier extends string, Options>
|
|
67
|
+
extends DatabaseServiceHolder<Contract, Identifier> {
|
|
68
|
+
readonly layer: (options: Options) => Layer.Layer<DatabaseIdentifier<Contract, Identifier>, PrismaError>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const isObjectLike = (value: unknown): value is object => (typeof value === "object" && value !== null) || typeof value === "function";
|
|
2
|
+
|
|
3
|
+
export const hasMethod = (value: unknown, name: PropertyKey): value is object =>
|
|
4
|
+
isObjectLike(value) && typeof Reflect.get(value, name) === "function";
|
|
5
|
+
|
|
6
|
+
export const invokeMethod = (target: object, name: PropertyKey, arguments_: ReadonlyArray<unknown>): unknown => {
|
|
7
|
+
const method: unknown = Reflect.get(target, name);
|
|
8
|
+
if (typeof method !== "function") {
|
|
9
|
+
throw new TypeError(`${String(name)} is not a method`);
|
|
10
|
+
}
|
|
11
|
+
return Reflect.apply(method, target, arguments_);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const isPromiseLike = (value: unknown): value is PromiseLike<unknown> => hasMethod(value, "then");
|
|
15
|
+
|
|
16
|
+
export const isAsyncIterable = (value: unknown): value is AsyncIterable<unknown> => hasMethod(value, Symbol.asyncIterator);
|
package/src/internal/executor.ts
CHANGED
|
@@ -17,10 +17,7 @@ export interface SqlDatabaseClient<Contract extends AnySqlContract> {
|
|
|
17
17
|
close(): Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface DatabaseExecutor<
|
|
21
|
-
Models extends object,
|
|
22
|
-
Contract extends AnySqlContract = AnySqlContract,
|
|
23
|
-
> {
|
|
20
|
+
export interface DatabaseExecutor<Models extends object, Contract extends AnySqlContract = AnySqlContract> {
|
|
24
21
|
readonly client: SqlDatabaseClient<Contract>;
|
|
25
22
|
readonly identity: object;
|
|
26
23
|
readonly liveness: {
|
package/src/internal/promise.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import { isPrismaFailure, type PrismaError, toPrismaError } from "../error.
|
|
2
|
+
import { isPrismaFailure, type PrismaError, toPrismaError } from "../error.ts";
|
|
3
3
|
|
|
4
|
-
export const fromPrismaPromise = <A>(
|
|
5
|
-
evaluate: (signal: AbortSignal) => PromiseLike<A>,
|
|
6
|
-
): Effect.Effect<A, PrismaError> =>
|
|
4
|
+
export const fromPrismaPromise = <A>(evaluate: (signal: AbortSignal) => PromiseLike<A>): Effect.Effect<A, PrismaError> =>
|
|
7
5
|
Effect.tryPromise({
|
|
8
6
|
try: (signal) => Promise.resolve(evaluate(signal)),
|
|
9
7
|
catch: (error) => error,
|
|
10
|
-
}).pipe(
|
|
11
|
-
Effect.catch((error) =>
|
|
12
|
-
isPrismaFailure(error)
|
|
13
|
-
? Effect.fail(toPrismaError(error))
|
|
14
|
-
: Effect.die(error),
|
|
15
|
-
),
|
|
16
|
-
);
|
|
8
|
+
}).pipe(Effect.catch((error) => (isPrismaFailure(error) ? Effect.fail(toPrismaError(error)) : Effect.die(error))));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import { type PrismaError, toPrismaError } from "../error.
|
|
3
|
-
import type { DatabaseExecutor } from "./executor.
|
|
2
|
+
import { type PrismaError, toPrismaError } from "../error.ts";
|
|
3
|
+
import type { DatabaseExecutor } from "./executor.ts";
|
|
4
4
|
|
|
5
5
|
export const executeQuery = <A, E, R>(
|
|
6
6
|
executor: Pick<DatabaseExecutor<object>, "liveness" | "querySemaphore">,
|
|
@@ -8,11 +8,7 @@ export const executeQuery = <A, E, R>(
|
|
|
8
8
|
): Effect.Effect<A, E | PrismaError, R> => {
|
|
9
9
|
const checked = Effect.suspend(
|
|
10
10
|
(): Effect.Effect<A, E | PrismaError, R> =>
|
|
11
|
-
executor.liveness.open
|
|
12
|
-
? effect
|
|
13
|
-
: Effect.fail(toPrismaError({ code: executor.liveness.closedCode })),
|
|
11
|
+
executor.liveness.open ? effect : Effect.fail(toPrismaError({ code: executor.liveness.closedCode })),
|
|
14
12
|
);
|
|
15
|
-
return executor.querySemaphore === undefined
|
|
16
|
-
? checked
|
|
17
|
-
: executor.querySemaphore.withPermit(Effect.uninterruptible(checked));
|
|
13
|
+
return executor.querySemaphore === undefined ? checked : executor.querySemaphore.withPermit(Effect.uninterruptible(checked));
|
|
18
14
|
};
|
package/src/internal/recipe.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hasMethod, invokeMethod } from "./dynamic.ts";
|
|
2
|
+
import { getRelationPlan, type RelationPlan } from "./relation-plan.ts";
|
|
2
3
|
|
|
3
4
|
export interface RelationOperation {
|
|
4
5
|
readonly name: PropertyKey;
|
|
@@ -13,11 +14,7 @@ export interface RelationRecipe {
|
|
|
13
14
|
|
|
14
15
|
export const rootRecipe = (model: string): RelationRecipe => ({ model });
|
|
15
16
|
|
|
16
|
-
export const appendOperation = (
|
|
17
|
-
parent: RelationRecipe,
|
|
18
|
-
name: PropertyKey,
|
|
19
|
-
arguments_: ReadonlyArray<unknown>,
|
|
20
|
-
): RelationRecipe => ({
|
|
17
|
+
export const appendOperation = (parent: RelationRecipe, name: PropertyKey, arguments_: ReadonlyArray<unknown>): RelationRecipe => ({
|
|
21
18
|
model: parent.model,
|
|
22
19
|
parent,
|
|
23
20
|
operation: {
|
|
@@ -26,9 +23,7 @@ export const appendOperation = (
|
|
|
26
23
|
},
|
|
27
24
|
});
|
|
28
25
|
|
|
29
|
-
const operations = (
|
|
30
|
-
recipe: RelationRecipe,
|
|
31
|
-
): ReadonlyArray<RelationOperation> => {
|
|
26
|
+
const operations = (recipe: RelationRecipe): ReadonlyArray<RelationOperation> => {
|
|
32
27
|
const reversed: Array<RelationOperation> = [];
|
|
33
28
|
let current: RelationRecipe | undefined = recipe;
|
|
34
29
|
|
|
@@ -42,81 +37,43 @@ const operations = (
|
|
|
42
37
|
return reversed.reverse();
|
|
43
38
|
};
|
|
44
39
|
|
|
45
|
-
const applyMethod = (
|
|
46
|
-
current
|
|
47
|
-
name: PropertyKey,
|
|
48
|
-
arguments_: ReadonlyArray<unknown>,
|
|
49
|
-
model: string,
|
|
50
|
-
): unknown => {
|
|
51
|
-
if (
|
|
52
|
-
typeof current !== "object" ||
|
|
53
|
-
current === null ||
|
|
54
|
-
typeof Reflect.get(current, name) !== "function"
|
|
55
|
-
) {
|
|
40
|
+
const applyMethod = (current: unknown, name: PropertyKey, arguments_: ReadonlyArray<unknown>, model: string): unknown => {
|
|
41
|
+
if (!hasMethod(current, name)) {
|
|
56
42
|
throw new TypeError(`Cannot call ${String(name)} while replaying ${model}`);
|
|
57
43
|
}
|
|
58
|
-
|
|
59
|
-
const method = Reflect.get(current, name) as (
|
|
60
|
-
...arguments_: ReadonlyArray<unknown>
|
|
61
|
-
) => unknown;
|
|
62
|
-
return Reflect.apply(method, current, arguments_);
|
|
44
|
+
return invokeMethod(current, name, arguments_);
|
|
63
45
|
};
|
|
64
46
|
|
|
65
|
-
const replayPlan = (
|
|
66
|
-
collection: unknown,
|
|
67
|
-
plan: RelationPlan,
|
|
68
|
-
owner: object,
|
|
69
|
-
transactionIdentity: object | undefined,
|
|
70
|
-
): unknown => {
|
|
47
|
+
const replayPlan = (collection: unknown, plan: RelationPlan, owner: object, transactionIdentity: object | undefined): unknown => {
|
|
71
48
|
if (!plan.liveness.open) {
|
|
72
49
|
throw new TypeError("Included Relation is closed");
|
|
73
50
|
}
|
|
74
51
|
if (plan.owner !== owner) {
|
|
75
52
|
throw new TypeError("Included Relations must use the same Database");
|
|
76
53
|
}
|
|
77
|
-
if (
|
|
78
|
-
plan.transactionIdentity !== undefined &&
|
|
79
|
-
plan.transactionIdentity !== transactionIdentity
|
|
80
|
-
) {
|
|
54
|
+
if (plan.transactionIdentity !== undefined && plan.transactionIdentity !== transactionIdentity) {
|
|
81
55
|
throw new TypeError("Included Relation belongs to another transaction");
|
|
82
56
|
}
|
|
83
|
-
const relatedModel =
|
|
84
|
-
typeof collection === "object" && collection !== null
|
|
85
|
-
? Reflect.get(collection, "modelName")
|
|
86
|
-
: undefined;
|
|
57
|
+
const relatedModel = typeof collection === "object" && collection !== null ? Reflect.get(collection, "modelName") : undefined;
|
|
87
58
|
if (typeof relatedModel === "string" && relatedModel !== plan.recipe.model) {
|
|
88
|
-
throw new TypeError(
|
|
89
|
-
`Included relation expects ${relatedModel}, received ${plan.recipe.model}`,
|
|
90
|
-
);
|
|
59
|
+
throw new TypeError(`Included relation expects ${relatedModel}, received ${plan.recipe.model}`);
|
|
91
60
|
}
|
|
92
61
|
|
|
93
|
-
const refined = replayRecipeFrom(
|
|
94
|
-
collection,
|
|
95
|
-
plan.recipe,
|
|
96
|
-
owner,
|
|
97
|
-
transactionIdentity,
|
|
98
|
-
);
|
|
62
|
+
const refined = replayRecipeFrom(collection, plan.recipe, owner, transactionIdentity);
|
|
99
63
|
if (plan.terminal !== "count") {
|
|
100
64
|
return refined;
|
|
101
65
|
}
|
|
102
66
|
return applyMethod(refined, "count", [], plan.recipe.model);
|
|
103
67
|
};
|
|
104
68
|
|
|
105
|
-
const includeRefinement = (
|
|
106
|
-
value: unknown,
|
|
107
|
-
owner: object,
|
|
108
|
-
transactionIdentity: object | undefined,
|
|
109
|
-
): ((collection: unknown) => unknown) => {
|
|
69
|
+
const includeRefinement = (value: unknown, owner: object, transactionIdentity: object | undefined): ((collection: unknown) => unknown) => {
|
|
110
70
|
const plan = getRelationPlan(value);
|
|
111
71
|
if (plan !== undefined) {
|
|
112
|
-
return (collection) =>
|
|
113
|
-
replayPlan(collection, plan, owner, transactionIdentity);
|
|
72
|
+
return (collection) => replayPlan(collection, plan, owner, transactionIdentity);
|
|
114
73
|
}
|
|
115
74
|
|
|
116
75
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
117
|
-
throw new TypeError(
|
|
118
|
-
"An included relation must be a Relation or query record",
|
|
119
|
-
);
|
|
76
|
+
throw new TypeError("An included relation must be a Relation or query record");
|
|
120
77
|
}
|
|
121
78
|
|
|
122
79
|
const entries = Object.entries(value);
|
|
@@ -137,46 +94,24 @@ const includeRefinement = (
|
|
|
137
94
|
}
|
|
138
95
|
|
|
139
96
|
return (collection) => {
|
|
140
|
-
const branches = Object.fromEntries(
|
|
141
|
-
plans.map(([name, queryPlan]) => [
|
|
142
|
-
name,
|
|
143
|
-
replayPlan(collection, queryPlan, owner, transactionIdentity),
|
|
144
|
-
]),
|
|
145
|
-
);
|
|
97
|
+
const branches = Object.fromEntries(plans.map(([name, queryPlan]) => [name, replayPlan(collection, queryPlan, owner, transactionIdentity)]));
|
|
146
98
|
return applyMethod(collection, "combine", [branches], model ?? "relation");
|
|
147
99
|
};
|
|
148
100
|
};
|
|
149
101
|
|
|
150
|
-
const replayRecipeFrom = (
|
|
151
|
-
root: unknown,
|
|
152
|
-
recipe: RelationRecipe,
|
|
153
|
-
owner: object,
|
|
154
|
-
transactionIdentity: object | undefined,
|
|
155
|
-
): unknown => {
|
|
102
|
+
const replayRecipeFrom = (root: unknown, recipe: RelationRecipe, owner: object, transactionIdentity: object | undefined): unknown => {
|
|
156
103
|
let current = root;
|
|
157
104
|
for (const operation of operations(recipe)) {
|
|
158
105
|
const arguments_ =
|
|
159
106
|
operation.name === "include" && operation.arguments.length === 2
|
|
160
|
-
? [
|
|
161
|
-
operation.arguments[0],
|
|
162
|
-
includeRefinement(
|
|
163
|
-
operation.arguments[1],
|
|
164
|
-
owner,
|
|
165
|
-
transactionIdentity,
|
|
166
|
-
),
|
|
167
|
-
]
|
|
107
|
+
? [operation.arguments[0], includeRefinement(operation.arguments[1], owner, transactionIdentity)]
|
|
168
108
|
: operation.arguments;
|
|
169
109
|
current = applyMethod(current, operation.name, arguments_, recipe.model);
|
|
170
110
|
}
|
|
171
111
|
return current;
|
|
172
112
|
};
|
|
173
113
|
|
|
174
|
-
export const replayRecipe = (
|
|
175
|
-
models: object,
|
|
176
|
-
recipe: RelationRecipe,
|
|
177
|
-
owner: object,
|
|
178
|
-
transactionIdentity: object | undefined,
|
|
179
|
-
): unknown => {
|
|
114
|
+
export const replayRecipe = (models: object, recipe: RelationRecipe, owner: object, transactionIdentity: object | undefined): unknown => {
|
|
180
115
|
const current: unknown = Reflect.get(models, recipe.model);
|
|
181
116
|
|
|
182
117
|
if (current === undefined) {
|