@shivaedev/effect-prisma 0.0.0 → 0.1.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 +14 -0
- package/LICENSE +21 -0
- package/README.md +120 -2
- package/dist/database.d.ts +35 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +83 -0
- package/dist/database.js.map +1 -0
- package/dist/error.d.ts +52 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +48 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/client-lifecycle.d.ts +7 -0
- package/dist/internal/client-lifecycle.d.ts.map +1 -0
- package/dist/internal/client-lifecycle.js +19 -0
- package/dist/internal/client-lifecycle.js.map +1 -0
- package/dist/internal/executor.d.ts +16 -0
- package/dist/internal/executor.d.ts.map +1 -0
- package/dist/internal/executor.js +2 -0
- package/dist/internal/executor.js.map +1 -0
- package/dist/internal/promise.d.ts +4 -0
- package/dist/internal/promise.d.ts.map +1 -0
- package/dist/internal/promise.js +9 -0
- package/dist/internal/promise.js.map +1 -0
- package/dist/internal/recipe.d.ts +13 -0
- package/dist/internal/recipe.d.ts.map +1 -0
- package/dist/internal/recipe.js +37 -0
- package/dist/internal/recipe.js.map +1 -0
- package/dist/internal/relation-runtime.d.ts +5 -0
- package/dist/internal/relation-runtime.d.ts.map +1 -0
- package/dist/internal/relation-runtime.js +88 -0
- package/dist/internal/relation-runtime.js.map +1 -0
- package/dist/internal/testing.d.ts +11 -0
- package/dist/internal/testing.d.ts.map +1 -0
- package/dist/internal/testing.js +9 -0
- package/dist/internal/testing.js.map +1 -0
- package/dist/internal/transaction.d.ts +14 -0
- package/dist/internal/transaction.d.ts.map +1 -0
- package/dist/internal/transaction.js +79 -0
- package/dist/internal/transaction.js.map +1 -0
- package/dist/internal/vitest-database.d.ts +18 -0
- package/dist/internal/vitest-database.d.ts.map +1 -0
- package/dist/internal/vitest-database.js +73 -0
- package/dist/internal/vitest-database.js.map +1 -0
- package/dist/relation/prisma-methods.d.ts +53 -0
- package/dist/relation/prisma-methods.d.ts.map +1 -0
- package/dist/relation/prisma-methods.js +2 -0
- package/dist/relation/prisma-methods.js.map +1 -0
- package/dist/relation.d.ts +27 -0
- package/dist/relation.d.ts.map +1 -0
- package/dist/relation.js +2 -0
- package/dist/relation.js.map +1 -0
- package/dist/testing/transaction.d.ts +10 -0
- package/dist/testing/transaction.d.ts.map +1 -0
- package/dist/testing/transaction.js +6 -0
- package/dist/testing/transaction.js.map +1 -0
- package/dist/testing/types.d.ts +29 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/testing/vitest.d.ts +6 -0
- package/dist/testing/vitest.d.ts.map +1 -0
- package/dist/testing/vitest.js +28 -0
- package/dist/testing/vitest.js.map +1 -0
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/package.json +69 -4
- package/src/database.ts +232 -0
- package/src/error.ts +89 -0
- package/src/index.ts +14 -0
- package/src/internal/client-lifecycle.ts +31 -0
- package/src/internal/executor.ts +28 -0
- package/src/internal/promise.ts +16 -0
- package/src/internal/recipe.ts +71 -0
- package/src/internal/relation-runtime.ts +217 -0
- package/src/internal/testing.ts +32 -0
- package/src/internal/transaction.ts +149 -0
- package/src/internal/vitest-database.ts +210 -0
- package/src/relation/prisma-methods.ts +192 -0
- package/src/relation.ts +104 -0
- package/src/testing/transaction.ts +28 -0
- package/src/testing/types.ts +67 -0
- package/src/testing/vitest.ts +67 -0
- package/src/testing.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-prisma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Effect-native PostgreSQL queries and transactions for Prisma Next",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,10 +9,75 @@
|
|
|
9
9
|
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
10
10
|
"directory": "packages/effect-prisma"
|
|
11
11
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
12
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-prisma#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=24"
|
|
14
18
|
},
|
|
19
|
+
"sideEffects": false,
|
|
15
20
|
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"CHANGELOG.md",
|
|
16
24
|
"README.md"
|
|
17
|
-
]
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"source": "./src/index.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./testing": {
|
|
34
|
+
"types": "./dist/testing.d.ts",
|
|
35
|
+
"source": "./src/testing.ts",
|
|
36
|
+
"import": "./dist/testing.js",
|
|
37
|
+
"default": "./dist/testing.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"provenance": true
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@prisma-next/contract": "0.15.0",
|
|
47
|
+
"@prisma-next/postgres": "0.15.0",
|
|
48
|
+
"@prisma-next/sql-contract": "0.15.0",
|
|
49
|
+
"@prisma-next/sql-orm-client": "0.15.0",
|
|
50
|
+
"@prisma-next/sql-relational-core": "0.15.0",
|
|
51
|
+
"@prisma-next/sql-runtime": "0.15.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@effect/vitest": "4.0.0-beta.99",
|
|
55
|
+
"effect": "4.0.0-beta.99"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@effect/vitest": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@effect/vitest": "4.0.0-beta.99",
|
|
64
|
+
"@prisma-next/adapter-postgres": "0.15.0",
|
|
65
|
+
"@prisma-next/cli": "0.15.0",
|
|
66
|
+
"@prisma-next/target-postgres": "0.15.0",
|
|
67
|
+
"@types/node": "24.10.1",
|
|
68
|
+
"@types/pg": "8.20.0",
|
|
69
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
70
|
+
"effect": "4.0.0-beta.99",
|
|
71
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
72
|
+
"vitest": "4.1.9"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
|
|
76
|
+
"check": "biome check .",
|
|
77
|
+
"test": "vitest run",
|
|
78
|
+
"test:package": "node scripts/test-package.mjs",
|
|
79
|
+
"typecheck": "tsc --noEmit",
|
|
80
|
+
"typecheck:compat": "tsc6 --noEmit",
|
|
81
|
+
"ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
|
|
82
|
+
}
|
|
18
83
|
}
|
package/src/database.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import postgres, {
|
|
2
|
+
type PostgresClient,
|
|
3
|
+
type PostgresOptionsBase,
|
|
4
|
+
} from "@prisma-next/postgres/runtime";
|
|
5
|
+
import { Context, Effect, Layer, Redacted } from "effect";
|
|
6
|
+
import type { PrismaError } from "./error.js";
|
|
7
|
+
import {
|
|
8
|
+
acquireConnectedClient,
|
|
9
|
+
assertAvailableModelNames,
|
|
10
|
+
} from "./internal/client-lifecycle.js";
|
|
11
|
+
import type {
|
|
12
|
+
AnyPostgresContract,
|
|
13
|
+
DatabaseExecutor,
|
|
14
|
+
ExecutorIdentifier,
|
|
15
|
+
} from "./internal/executor.js";
|
|
16
|
+
import { fromPrismaPromise } from "./internal/promise.js";
|
|
17
|
+
import { makeModelRelation } from "./internal/relation-runtime.js";
|
|
18
|
+
import { DatabaseTestingTypeId } from "./internal/testing.js";
|
|
19
|
+
import {
|
|
20
|
+
acquireTransaction,
|
|
21
|
+
releaseTestTransaction,
|
|
22
|
+
releaseTransaction,
|
|
23
|
+
} from "./internal/transaction.js";
|
|
24
|
+
import type { Relation } from "./relation.js";
|
|
25
|
+
|
|
26
|
+
type ClientFor<Contract extends AnyPostgresContract> = PostgresClient<Contract>;
|
|
27
|
+
type OrmFor<Contract extends AnyPostgresContract> = ClientFor<Contract>["orm"];
|
|
28
|
+
type DefaultNamespaceId<Contract extends AnyPostgresContract> =
|
|
29
|
+
"__unbound__" extends keyof OrmFor<Contract>
|
|
30
|
+
? "__unbound__"
|
|
31
|
+
: "public" extends keyof OrmFor<Contract>
|
|
32
|
+
? "public"
|
|
33
|
+
: keyof OrmFor<Contract>;
|
|
34
|
+
type DefaultModels<Contract extends AnyPostgresContract> =
|
|
35
|
+
OrmFor<Contract>[DefaultNamespaceId<Contract>] extends infer Models extends
|
|
36
|
+
object
|
|
37
|
+
? Models
|
|
38
|
+
: never;
|
|
39
|
+
|
|
40
|
+
interface DatabaseIdentifier<Contract extends AnyPostgresContract> {
|
|
41
|
+
readonly _contract: Contract;
|
|
42
|
+
readonly _databaseIdentifier: unique symbol;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type DatabaseModels<Contract extends AnyPostgresContract, Requirement> = {
|
|
46
|
+
readonly [Model in keyof DefaultModels<Contract>]: Relation<
|
|
47
|
+
DefaultModels<Contract>[Model],
|
|
48
|
+
Requirement,
|
|
49
|
+
Contract,
|
|
50
|
+
Model & string
|
|
51
|
+
>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type DatabaseService<
|
|
55
|
+
Contract extends AnyPostgresContract,
|
|
56
|
+
Requirement,
|
|
57
|
+
> = DatabaseModels<Contract, Requirement> & {
|
|
58
|
+
transaction<A, E, R>(
|
|
59
|
+
program: Effect.Effect<A, E, R>,
|
|
60
|
+
): Effect.Effect<A, E | PrismaError, R | Requirement>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export interface DatabaseLayerOptions extends PostgresOptionsBase {
|
|
64
|
+
readonly url: string | Redacted.Redacted<string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type DatabaseFactoryOptions<Contract extends AnyPostgresContract> =
|
|
68
|
+
| {
|
|
69
|
+
readonly contract: Contract;
|
|
70
|
+
readonly contractJson?: never;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
readonly contractJson: unknown;
|
|
74
|
+
readonly contract?: never;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface DatabaseDefinition<
|
|
78
|
+
Contract extends AnyPostgresContract,
|
|
79
|
+
Requirement = ExecutorIdentifier<DefaultModels<Contract>>,
|
|
80
|
+
> extends Context.Service<
|
|
81
|
+
DatabaseIdentifier<Contract>,
|
|
82
|
+
DatabaseService<Contract, Requirement>
|
|
83
|
+
> {
|
|
84
|
+
readonly layer: (
|
|
85
|
+
options: DatabaseLayerOptions,
|
|
86
|
+
) => Layer.Layer<DatabaseIdentifier<Contract> | Requirement, PrismaError>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const defaultModels = <
|
|
90
|
+
Contract extends AnyPostgresContract,
|
|
91
|
+
Models extends object,
|
|
92
|
+
>(
|
|
93
|
+
client: Pick<PostgresClient<Contract>, "contract" | "orm">,
|
|
94
|
+
): Models => {
|
|
95
|
+
const namespaces = Object.keys(client.contract.domain.namespaces);
|
|
96
|
+
if (namespaces.length !== 1 || namespaces[0] === undefined) {
|
|
97
|
+
throw new TypeError(
|
|
98
|
+
"Effect Prisma currently requires exactly one domain namespace",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
assertAvailableModelNames(
|
|
102
|
+
Object.keys(client.contract.domain.namespaces[namespaces[0]].models),
|
|
103
|
+
);
|
|
104
|
+
return Reflect.get(client.orm, namespaces[0]) as Models;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const makeDatabase = <const Contract extends AnyPostgresContract>(
|
|
108
|
+
identifier: string,
|
|
109
|
+
options: DatabaseFactoryOptions<Contract>,
|
|
110
|
+
): DatabaseDefinition<Contract> => {
|
|
111
|
+
type Models = DefaultModels<Contract>;
|
|
112
|
+
type ExecutorId = ExecutorIdentifier<Models>;
|
|
113
|
+
type DatabaseId = DatabaseIdentifier<Contract>;
|
|
114
|
+
|
|
115
|
+
const Executor = Context.Service<
|
|
116
|
+
ExecutorId,
|
|
117
|
+
DatabaseExecutor<Models, Contract>
|
|
118
|
+
>(`${identifier}/Executor`);
|
|
119
|
+
const Service = Context.Service<
|
|
120
|
+
DatabaseId,
|
|
121
|
+
DatabaseService<Contract, ExecutorId>
|
|
122
|
+
>(identifier);
|
|
123
|
+
|
|
124
|
+
const transaction = <A, E, R>(
|
|
125
|
+
program: Effect.Effect<A, E, R>,
|
|
126
|
+
): Effect.Effect<A, E | PrismaError, R | ExecutorId> =>
|
|
127
|
+
Effect.flatMap(Executor, (current) => {
|
|
128
|
+
if (current.transactional) {
|
|
129
|
+
return program;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Effect.acquireUseRelease(
|
|
133
|
+
acquireTransaction(current, (transactionOrm) =>
|
|
134
|
+
defaultModels<Contract, Models>({
|
|
135
|
+
contract: current.client.contract,
|
|
136
|
+
orm: transactionOrm,
|
|
137
|
+
}),
|
|
138
|
+
),
|
|
139
|
+
(resource) =>
|
|
140
|
+
program.pipe(Effect.provideService(Executor, resource.executor)),
|
|
141
|
+
releaseTransaction,
|
|
142
|
+
).pipe(Effect.withSpan("prisma.transaction", { kind: "client" }));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const withTestTransaction = <A, E, R>(
|
|
146
|
+
program: Effect.Effect<A, E, R>,
|
|
147
|
+
): Effect.Effect<A, E | PrismaError, R | ExecutorId> =>
|
|
148
|
+
Effect.flatMap(Executor, (current) => {
|
|
149
|
+
if (current.transactional) {
|
|
150
|
+
return program;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return Effect.acquireUseRelease(
|
|
154
|
+
acquireTransaction(current, (transactionOrm) =>
|
|
155
|
+
defaultModels<Contract, Models>({
|
|
156
|
+
contract: current.client.contract,
|
|
157
|
+
orm: transactionOrm,
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
(resource) =>
|
|
161
|
+
program.pipe(Effect.provideService(Executor, resource.executor)),
|
|
162
|
+
releaseTestTransaction,
|
|
163
|
+
).pipe(Effect.withSpan("prisma.testTransaction", { kind: "client" }));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const facade = new Proxy(
|
|
167
|
+
Object.assign(Object.create(null), {
|
|
168
|
+
transaction,
|
|
169
|
+
}) as DatabaseService<Contract, ExecutorId>,
|
|
170
|
+
{
|
|
171
|
+
get(target, property, receiver) {
|
|
172
|
+
if (Reflect.has(target, property)) {
|
|
173
|
+
return Reflect.get(target, property, receiver);
|
|
174
|
+
}
|
|
175
|
+
if (typeof property !== "string") {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return makeModelRelation(Executor, property);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const layer = (
|
|
184
|
+
layerOptions: DatabaseLayerOptions,
|
|
185
|
+
): Layer.Layer<DatabaseId | ExecutorId, PrismaError> => {
|
|
186
|
+
const acquire = Effect.acquireRelease(
|
|
187
|
+
Effect.suspend(() => {
|
|
188
|
+
const clientOptions = {
|
|
189
|
+
url:
|
|
190
|
+
typeof layerOptions.url === "string"
|
|
191
|
+
? layerOptions.url
|
|
192
|
+
: Redacted.value(layerOptions.url),
|
|
193
|
+
extensions: layerOptions.extensions,
|
|
194
|
+
middleware: layerOptions.middleware,
|
|
195
|
+
poolOptions: layerOptions.poolOptions,
|
|
196
|
+
verifyMarker: layerOptions.verifyMarker,
|
|
197
|
+
};
|
|
198
|
+
const client =
|
|
199
|
+
options.contract === undefined
|
|
200
|
+
? postgres<Contract>({
|
|
201
|
+
...clientOptions,
|
|
202
|
+
contractJson: options.contractJson,
|
|
203
|
+
})
|
|
204
|
+
: postgres<Contract>({
|
|
205
|
+
...clientOptions,
|
|
206
|
+
contract: options.contract,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return fromPrismaPromise(() =>
|
|
210
|
+
acquireConnectedClient(client, () => ({
|
|
211
|
+
client,
|
|
212
|
+
models: defaultModels<Contract, Models>(client),
|
|
213
|
+
transactional: false,
|
|
214
|
+
})),
|
|
215
|
+
);
|
|
216
|
+
}),
|
|
217
|
+
(executor) =>
|
|
218
|
+
Effect.promise(() => executor.client.close()).pipe(Effect.orDie),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return Layer.effectContext(
|
|
222
|
+
Effect.map(acquire, (executor) =>
|
|
223
|
+
Context.make(Executor, executor).pipe(Context.add(Service, facade)),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return Object.assign(Service, {
|
|
229
|
+
layer,
|
|
230
|
+
[DatabaseTestingTypeId]: { withTestTransaction },
|
|
231
|
+
}) as DatabaseDefinition<Contract>;
|
|
232
|
+
};
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Data, Redacted } from "effect";
|
|
2
|
+
|
|
3
|
+
export class PrismaRuntimeFailure extends Data.TaggedClass(
|
|
4
|
+
"PrismaRuntimeFailure",
|
|
5
|
+
)<{
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
8
|
+
}> {}
|
|
9
|
+
|
|
10
|
+
export class PrismaQueryFailure extends Data.TaggedClass("PrismaQueryFailure")<{
|
|
11
|
+
readonly sqlState?: string;
|
|
12
|
+
readonly constraint?: string;
|
|
13
|
+
readonly table?: string;
|
|
14
|
+
readonly column?: string;
|
|
15
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
16
|
+
}> {}
|
|
17
|
+
|
|
18
|
+
export class PrismaConnectionFailure extends Data.TaggedClass(
|
|
19
|
+
"PrismaConnectionFailure",
|
|
20
|
+
)<{
|
|
21
|
+
readonly transient?: boolean;
|
|
22
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
23
|
+
}> {}
|
|
24
|
+
|
|
25
|
+
export type PrismaErrorReason =
|
|
26
|
+
| PrismaRuntimeFailure
|
|
27
|
+
| PrismaQueryFailure
|
|
28
|
+
| PrismaConnectionFailure;
|
|
29
|
+
|
|
30
|
+
export class PrismaError extends Data.TaggedError("PrismaError")<{
|
|
31
|
+
readonly reason: PrismaErrorReason;
|
|
32
|
+
}> {}
|
|
33
|
+
|
|
34
|
+
type PrismaFailure =
|
|
35
|
+
| { readonly code: string }
|
|
36
|
+
| {
|
|
37
|
+
readonly kind: "sql_query";
|
|
38
|
+
readonly sqlState?: string;
|
|
39
|
+
readonly constraint?: string;
|
|
40
|
+
readonly table?: string;
|
|
41
|
+
readonly column?: string;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
readonly kind: "sql_connection";
|
|
45
|
+
readonly transient?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const isPrismaFailure = (error: unknown): error is PrismaFailure => {
|
|
49
|
+
if (typeof error !== "object" || error === null) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if ("code" in error && typeof error.code === "string") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
"kind" in error &&
|
|
57
|
+
(error.kind === "sql_query" || error.kind === "sql_connection")
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const toPrismaError = (error: PrismaFailure): PrismaError => {
|
|
62
|
+
const original = Redacted.make(error);
|
|
63
|
+
|
|
64
|
+
if ("kind" in error && error.kind === "sql_query") {
|
|
65
|
+
return new PrismaError({
|
|
66
|
+
reason: new PrismaQueryFailure({
|
|
67
|
+
sqlState: error.sqlState,
|
|
68
|
+
constraint: error.constraint,
|
|
69
|
+
table: error.table,
|
|
70
|
+
column: error.column,
|
|
71
|
+
original,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if ("kind" in error && error.kind === "sql_connection") {
|
|
76
|
+
return new PrismaError({
|
|
77
|
+
reason: new PrismaConnectionFailure({
|
|
78
|
+
transient: error.transient,
|
|
79
|
+
original,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return new PrismaError({
|
|
84
|
+
reason: new PrismaRuntimeFailure({
|
|
85
|
+
code: error.code,
|
|
86
|
+
original,
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type DatabaseDefinition,
|
|
3
|
+
type DatabaseLayerOptions,
|
|
4
|
+
type DatabaseService,
|
|
5
|
+
makeDatabase,
|
|
6
|
+
} from "./database.js";
|
|
7
|
+
export {
|
|
8
|
+
PrismaConnectionFailure,
|
|
9
|
+
PrismaError,
|
|
10
|
+
type PrismaErrorReason,
|
|
11
|
+
PrismaQueryFailure,
|
|
12
|
+
PrismaRuntimeFailure,
|
|
13
|
+
} from "./error.js";
|
|
14
|
+
export type { Relation } from "./relation.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ClientLifecycle {
|
|
2
|
+
readonly connect: () => PromiseLike<unknown>;
|
|
3
|
+
readonly close: () => PromiseLike<void>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const acquireConnectedClient = async <A>(
|
|
7
|
+
client: ClientLifecycle,
|
|
8
|
+
initialize: () => A,
|
|
9
|
+
): Promise<A> => {
|
|
10
|
+
try {
|
|
11
|
+
await client.connect();
|
|
12
|
+
return initialize();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
await Promise.resolve(client.close()).catch(() => undefined);
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const reservedModelNames = new Set(["transaction"]);
|
|
20
|
+
|
|
21
|
+
export const assertAvailableModelNames = (
|
|
22
|
+
modelNames: ReadonlyArray<string>,
|
|
23
|
+
): void => {
|
|
24
|
+
for (const modelName of modelNames) {
|
|
25
|
+
if (reservedModelNames.has(modelName)) {
|
|
26
|
+
throw new TypeError(
|
|
27
|
+
`Prisma model name conflicts with the database facade: ${modelName}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Contract as PrismaContract } from "@prisma-next/contract/types";
|
|
2
|
+
import type { PostgresClient } from "@prisma-next/postgres/runtime";
|
|
3
|
+
import type { SqlStorage } from "@prisma-next/sql-contract/types";
|
|
4
|
+
import type { Context } from "effect";
|
|
5
|
+
|
|
6
|
+
export type AnyPostgresContract = PrismaContract<SqlStorage>;
|
|
7
|
+
|
|
8
|
+
export interface DatabaseExecutor<
|
|
9
|
+
Models extends object,
|
|
10
|
+
Contract extends AnyPostgresContract = AnyPostgresContract,
|
|
11
|
+
> {
|
|
12
|
+
readonly client: PostgresClient<Contract>;
|
|
13
|
+
readonly models: Models;
|
|
14
|
+
readonly transactional: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ExecutorService<
|
|
18
|
+
Models extends object,
|
|
19
|
+
Contract extends AnyPostgresContract = AnyPostgresContract,
|
|
20
|
+
> = Context.Service<
|
|
21
|
+
ExecutorIdentifier<Models>,
|
|
22
|
+
DatabaseExecutor<Models, Contract>
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
export interface ExecutorIdentifier<Models extends object> {
|
|
26
|
+
readonly _models: Models;
|
|
27
|
+
readonly _executorIdentifier: unique symbol;
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { isPrismaFailure, type PrismaError, toPrismaError } from "../error.js";
|
|
3
|
+
|
|
4
|
+
export const fromPrismaPromise = <A>(
|
|
5
|
+
evaluate: (signal: AbortSignal) => PromiseLike<A>,
|
|
6
|
+
): Effect.Effect<A, PrismaError> =>
|
|
7
|
+
Effect.tryPromise({
|
|
8
|
+
try: (signal) => Promise.resolve(evaluate(signal)),
|
|
9
|
+
catch: (error) => error,
|
|
10
|
+
}).pipe(
|
|
11
|
+
Effect.catch((error) =>
|
|
12
|
+
isPrismaFailure(error)
|
|
13
|
+
? Effect.fail(toPrismaError(error))
|
|
14
|
+
: Effect.die(error),
|
|
15
|
+
),
|
|
16
|
+
);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface RelationOperation {
|
|
2
|
+
readonly name: PropertyKey;
|
|
3
|
+
readonly arguments: ReadonlyArray<unknown>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RelationRecipe {
|
|
7
|
+
readonly model: string;
|
|
8
|
+
readonly parent?: RelationRecipe;
|
|
9
|
+
readonly operation?: RelationOperation;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const rootRecipe = (model: string): RelationRecipe => ({ model });
|
|
13
|
+
|
|
14
|
+
export const appendOperation = (
|
|
15
|
+
parent: RelationRecipe,
|
|
16
|
+
name: PropertyKey,
|
|
17
|
+
arguments_: ReadonlyArray<unknown>,
|
|
18
|
+
): RelationRecipe => ({
|
|
19
|
+
model: parent.model,
|
|
20
|
+
parent,
|
|
21
|
+
operation: {
|
|
22
|
+
name,
|
|
23
|
+
arguments: arguments_,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const operations = (
|
|
28
|
+
recipe: RelationRecipe,
|
|
29
|
+
): ReadonlyArray<RelationOperation> => {
|
|
30
|
+
const reversed: Array<RelationOperation> = [];
|
|
31
|
+
let current: RelationRecipe | undefined = recipe;
|
|
32
|
+
|
|
33
|
+
while (current !== undefined) {
|
|
34
|
+
if (current.operation !== undefined) {
|
|
35
|
+
reversed.push(current.operation);
|
|
36
|
+
}
|
|
37
|
+
current = current.parent;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return reversed.reverse();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const replayRecipe = (
|
|
44
|
+
models: object,
|
|
45
|
+
recipe: RelationRecipe,
|
|
46
|
+
): unknown => {
|
|
47
|
+
let current: unknown = Reflect.get(models, recipe.model);
|
|
48
|
+
|
|
49
|
+
if (current === undefined) {
|
|
50
|
+
throw new TypeError(`Unknown Prisma model: ${recipe.model}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const operation of operations(recipe)) {
|
|
54
|
+
if (
|
|
55
|
+
typeof current !== "object" ||
|
|
56
|
+
current === null ||
|
|
57
|
+
typeof Reflect.get(current, operation.name) !== "function"
|
|
58
|
+
) {
|
|
59
|
+
throw new TypeError(
|
|
60
|
+
`Cannot call ${String(operation.name)} while replaying ${recipe.model}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const method = Reflect.get(current, operation.name) as (
|
|
65
|
+
...arguments_: ReadonlyArray<unknown>
|
|
66
|
+
) => unknown;
|
|
67
|
+
current = Reflect.apply(method, current, operation.arguments);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return current;
|
|
71
|
+
};
|