@shivaedev/effect-prisma 0.4.3 → 0.5.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/README.md +37 -0
- package/dist/database.d.ts +8 -27
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +29 -78
- package/dist/database.js.map +1 -1
- package/dist/internal/database-factory.d.ts +46 -0
- package/dist/internal/database-factory.d.ts.map +1 -0
- package/dist/internal/database-factory.js +56 -0
- package/dist/internal/database-factory.js.map +1 -0
- package/dist/internal/executor.d.ts +16 -5
- package/dist/internal/executor.d.ts.map +1 -1
- package/dist/internal/sqlite-datetime.d.ts +31 -0
- package/dist/internal/sqlite-datetime.d.ts.map +1 -0
- package/dist/internal/sqlite-datetime.js +66 -0
- package/dist/internal/sqlite-datetime.js.map +1 -0
- package/dist/internal/sqlite-pragmas.d.ts +16 -0
- package/dist/internal/sqlite-pragmas.d.ts.map +1 -0
- package/dist/internal/sqlite-pragmas.js +34 -0
- package/dist/internal/sqlite-pragmas.js.map +1 -0
- package/dist/internal/transaction.d.ts +7 -6
- package/dist/internal/transaction.d.ts.map +1 -1
- package/dist/internal/transaction.js.map +1 -1
- package/dist/sqlite.d.ts +28 -0
- package/dist/sqlite.d.ts.map +1 -0
- package/dist/sqlite.js +41 -0
- package/dist/sqlite.js.map +1 -0
- package/package.json +11 -2
- package/src/database.ts +54 -204
- package/src/internal/database-factory.ts +209 -0
- package/src/internal/executor.ts +17 -5
- package/src/internal/sqlite-datetime.ts +95 -0
- package/src/internal/sqlite-pragmas.ts +41 -0
- package/src/internal/transaction.ts +11 -8
- package/src/sqlite.ts +108 -0
package/src/internal/executor.ts
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import type { Contract as PrismaContract } from "@prisma-next/contract/types";
|
|
2
|
-
import type { PostgresClient } from "@prisma-next/postgres/runtime";
|
|
3
2
|
import type { SqlStorage } from "@prisma-next/sql-contract/types";
|
|
3
|
+
import type { ExecutionContext, Runtime } from "@prisma-next/sql-runtime";
|
|
4
4
|
import type { Context, Semaphore } from "effect";
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type AnySqlContract = PrismaContract<SqlStorage>;
|
|
7
|
+
export type AnyPostgresContract = AnySqlContract;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The subset of a Prisma Next client the package depends on. Both the
|
|
11
|
+
* PostgreSQL and the SQLite client satisfy it.
|
|
12
|
+
*/
|
|
13
|
+
export interface SqlDatabaseClient<Contract extends AnySqlContract> {
|
|
14
|
+
readonly contract: Contract;
|
|
15
|
+
readonly context: ExecutionContext<Contract>;
|
|
16
|
+
runtime(): Runtime;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
export interface DatabaseExecutor<
|
|
9
21
|
Models extends object,
|
|
10
|
-
Contract extends
|
|
22
|
+
Contract extends AnySqlContract = AnySqlContract,
|
|
11
23
|
> {
|
|
12
|
-
readonly client:
|
|
24
|
+
readonly client: SqlDatabaseClient<Contract>;
|
|
13
25
|
readonly models: Models;
|
|
14
26
|
readonly querySemaphore: Semaphore.Semaphore | undefined;
|
|
15
27
|
readonly transactional: boolean;
|
|
@@ -17,7 +29,7 @@ export interface DatabaseExecutor<
|
|
|
17
29
|
|
|
18
30
|
export type ExecutorService<
|
|
19
31
|
Models extends object,
|
|
20
|
-
Contract extends
|
|
32
|
+
Contract extends AnySqlContract = AnySqlContract,
|
|
21
33
|
> = Context.Service<
|
|
22
34
|
ExecutorIdentifier<Models>,
|
|
23
35
|
DatabaseExecutor<Models, Contract>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ExecutionContext } from "@prisma-next/sql-runtime";
|
|
2
|
+
import type { AnySqlContract } from "./executor.js";
|
|
3
|
+
|
|
4
|
+
type CodecRegistry = ExecutionContext<AnySqlContract>["contractCodecs"];
|
|
5
|
+
type ContractCodec = ReturnType<CodecRegistry["forCodecRef"]>;
|
|
6
|
+
type CodecWire = Parameters<ContractCodec["decode"]>[0];
|
|
7
|
+
type CodecJson = Parameters<ContractCodec["decodeJson"]>[0];
|
|
8
|
+
|
|
9
|
+
const sqliteDatetimeCodecId = "sqlite/datetime@1";
|
|
10
|
+
|
|
11
|
+
const zonelessDatetime =
|
|
12
|
+
/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)$/;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Stamp `Z` onto a SQLite datetime string that carries no zone designator.
|
|
16
|
+
*
|
|
17
|
+
* SQLite stores `DateTime` as text and computes `datetime('now')` — the default
|
|
18
|
+
* `prisma-next db init` generates — in UTC, but writes it as
|
|
19
|
+
* `YYYY-MM-DD HH:MM:SS` with no zone. `new Date` reads that form as local time,
|
|
20
|
+
* so decoding it unchanged shifts the instant by the process' UTC offset.
|
|
21
|
+
* Values that already carry `Z` or a numeric offset, date-only values (which
|
|
22
|
+
* `new Date` already reads as UTC), and anything that is not a datetime string
|
|
23
|
+
* are returned unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export const normalizeSqliteDatetime = (value: string): string =>
|
|
26
|
+
value.replace(zonelessDatetime, "$1T$2Z");
|
|
27
|
+
|
|
28
|
+
const decodeAsUtc = (codec: ContractCodec): ContractCodec => ({
|
|
29
|
+
id: sqliteDatetimeCodecId,
|
|
30
|
+
encode: (value, context) => codec.encode(value, context),
|
|
31
|
+
decode: (wire: CodecWire, context) =>
|
|
32
|
+
codec.decode(
|
|
33
|
+
typeof wire === "string" ? normalizeSqliteDatetime(wire) : wire,
|
|
34
|
+
context,
|
|
35
|
+
),
|
|
36
|
+
encodeJson: (value) => codec.encodeJson(value),
|
|
37
|
+
decodeJson: (json: CodecJson) =>
|
|
38
|
+
codec.decodeJson(
|
|
39
|
+
typeof json === "string" ? normalizeSqliteDatetime(json) : json,
|
|
40
|
+
),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replace the client's codec registry with one that decodes zone-less
|
|
45
|
+
* `sqlite/datetime@1` values as UTC.
|
|
46
|
+
*
|
|
47
|
+
* The registry is the single resolution point for both row decoding and the
|
|
48
|
+
* JSON decoding of included relations, so wrapping it covers every read path
|
|
49
|
+
* without touching encoding. Prisma Next rejects a second descriptor for an
|
|
50
|
+
* already-registered codec id, so an extension pack cannot replace the codec
|
|
51
|
+
* itself. Must run before the first query, because the runtime reads the
|
|
52
|
+
* registry off the context when it is created.
|
|
53
|
+
*
|
|
54
|
+
* Codecs are identified by the resolved reference rather than by `codec.id`:
|
|
55
|
+
* Prisma Next materializes a codec through an unbound descriptor factory, so
|
|
56
|
+
* the instance's `id` accessor throws.
|
|
57
|
+
*/
|
|
58
|
+
export const decodeSqliteDatetimesAsUtc = (
|
|
59
|
+
context: ExecutionContext<AnySqlContract>,
|
|
60
|
+
): void => {
|
|
61
|
+
const registry = context.contractCodecs;
|
|
62
|
+
const descriptors = context.codecDescriptors;
|
|
63
|
+
const utcCodecs = new WeakMap<ContractCodec, ContractCodec>();
|
|
64
|
+
|
|
65
|
+
const wrap = (codec: ContractCodec): ContractCodec => {
|
|
66
|
+
const existing = utcCodecs.get(codec);
|
|
67
|
+
if (existing !== undefined) {
|
|
68
|
+
return existing;
|
|
69
|
+
}
|
|
70
|
+
const utc = decodeAsUtc(codec);
|
|
71
|
+
utcCodecs.set(codec, utc);
|
|
72
|
+
return utc;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const utcRegistry: CodecRegistry = {
|
|
76
|
+
forColumn: (namespaceId, table, column) => {
|
|
77
|
+
const codec = registry.forColumn(namespaceId, table, column);
|
|
78
|
+
if (codec === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const reference = descriptors.codecRefForColumn(
|
|
82
|
+
namespaceId,
|
|
83
|
+
table,
|
|
84
|
+
column,
|
|
85
|
+
);
|
|
86
|
+
return reference?.codecId === sqliteDatetimeCodecId ? wrap(codec) : codec;
|
|
87
|
+
},
|
|
88
|
+
forCodecRef: (reference) => {
|
|
89
|
+
const codec = registry.forCodecRef(reference);
|
|
90
|
+
return reference.codecId === sqliteDatetimeCodecId ? wrap(codec) : codec;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
(context as { contractCodecs: CodecRegistry }).contractCodecs = utcRegistry;
|
|
95
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
|
|
3
|
+
export const MEMORY_PATH = ":memory:";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prisma Next's SQLite driver opens a fresh `node:sqlite` connection for every
|
|
7
|
+
* `RuntimeConnection`, so an in-memory database would give each transaction its
|
|
8
|
+
* own empty database.
|
|
9
|
+
*/
|
|
10
|
+
export const assertFileBackedPath = (path: string): void => {
|
|
11
|
+
if (path === MEMORY_PATH || path.trim().length === 0) {
|
|
12
|
+
throw new TypeError(
|
|
13
|
+
"Effect Prisma requires a file-backed SQLite database; transactions run on their own connection and cannot see an in-memory database",
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Apply connect-time pragmas.
|
|
20
|
+
*
|
|
21
|
+
* The driver already sets `foreign_keys` and `busy_timeout` on every connection
|
|
22
|
+
* it opens, but it has no hook for anything else, so durable pragmas such as
|
|
23
|
+
* `journal_mode` are applied once against the database file itself.
|
|
24
|
+
*/
|
|
25
|
+
export const applySqlitePragmas = (
|
|
26
|
+
path: string,
|
|
27
|
+
pragmas: ReadonlyArray<string>,
|
|
28
|
+
): void => {
|
|
29
|
+
if (pragmas.length === 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const database = new DatabaseSync(path);
|
|
34
|
+
try {
|
|
35
|
+
for (const pragma of pragmas) {
|
|
36
|
+
database.exec(`PRAGMA ${pragma}`);
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
database.close();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { PostgresClient } from "@prisma-next/postgres/runtime";
|
|
2
1
|
import { orm } from "@prisma-next/sql-orm-client";
|
|
3
2
|
import type {
|
|
4
3
|
RuntimeConnection,
|
|
@@ -6,12 +5,16 @@ import type {
|
|
|
6
5
|
} from "@prisma-next/sql-runtime";
|
|
7
6
|
import { Effect, Exit, Semaphore } from "effect";
|
|
8
7
|
import type { PrismaError } from "../error.js";
|
|
9
|
-
import type {
|
|
8
|
+
import type { AnySqlContract, DatabaseExecutor } from "./executor.js";
|
|
10
9
|
import { fromPrismaPromise } from "./promise.js";
|
|
11
10
|
|
|
11
|
+
export type TransactionOrm<Contract extends AnySqlContract> = ReturnType<
|
|
12
|
+
typeof orm<Contract>
|
|
13
|
+
>;
|
|
14
|
+
|
|
12
15
|
export interface TransactionResource<
|
|
13
16
|
Models extends object,
|
|
14
|
-
Contract extends
|
|
17
|
+
Contract extends AnySqlContract,
|
|
15
18
|
> {
|
|
16
19
|
readonly connection: RuntimeConnection;
|
|
17
20
|
readonly transaction: RuntimeTransaction;
|
|
@@ -27,10 +30,10 @@ const runtimeFailure = (
|
|
|
27
30
|
|
|
28
31
|
export const acquireTransaction = <
|
|
29
32
|
Models extends object,
|
|
30
|
-
Contract extends
|
|
33
|
+
Contract extends AnySqlContract,
|
|
31
34
|
>(
|
|
32
35
|
current: DatabaseExecutor<Models, Contract>,
|
|
33
|
-
models: (orm:
|
|
36
|
+
models: (orm: TransactionOrm<Contract>) => Models,
|
|
34
37
|
): Effect.Effect<TransactionResource<Models, Contract>, PrismaError> =>
|
|
35
38
|
fromPrismaPromise(async () => {
|
|
36
39
|
const connection = await current.client.runtime().connection();
|
|
@@ -60,7 +63,7 @@ export const acquireTransaction = <
|
|
|
60
63
|
|
|
61
64
|
export const releaseTransaction = <
|
|
62
65
|
Models extends object,
|
|
63
|
-
Contract extends
|
|
66
|
+
Contract extends AnySqlContract,
|
|
64
67
|
A,
|
|
65
68
|
E,
|
|
66
69
|
>(
|
|
@@ -70,7 +73,7 @@ export const releaseTransaction = <
|
|
|
70
73
|
|
|
71
74
|
export const releaseTestTransaction = <
|
|
72
75
|
Models extends object,
|
|
73
|
-
Contract extends
|
|
76
|
+
Contract extends AnySqlContract,
|
|
74
77
|
A,
|
|
75
78
|
E,
|
|
76
79
|
>(
|
|
@@ -80,7 +83,7 @@ export const releaseTestTransaction = <
|
|
|
80
83
|
|
|
81
84
|
const settleTransaction = <
|
|
82
85
|
Models extends object,
|
|
83
|
-
Contract extends
|
|
86
|
+
Contract extends AnySqlContract,
|
|
84
87
|
A,
|
|
85
88
|
E,
|
|
86
89
|
>(
|
package/src/sqlite.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import sqlite, {
|
|
2
|
+
type SqliteClient,
|
|
3
|
+
type SqliteOptionsBase,
|
|
4
|
+
} from "@prisma-next/sqlite/runtime";
|
|
5
|
+
import type { Layer } from "effect";
|
|
6
|
+
import type { PrismaError } from "./error.js";
|
|
7
|
+
import {
|
|
8
|
+
acquireConnectedClient,
|
|
9
|
+
assertAvailableModelNames,
|
|
10
|
+
} from "./internal/client-lifecycle.js";
|
|
11
|
+
import {
|
|
12
|
+
type DatabaseIdentifier,
|
|
13
|
+
type DatabaseServiceHolder,
|
|
14
|
+
type DefaultModels,
|
|
15
|
+
makeSqlDatabase,
|
|
16
|
+
} from "./internal/database-factory.js";
|
|
17
|
+
import type {
|
|
18
|
+
AnySqlContract,
|
|
19
|
+
ExecutorIdentifier,
|
|
20
|
+
} from "./internal/executor.js";
|
|
21
|
+
import { fromPrismaPromise } from "./internal/promise.js";
|
|
22
|
+
import { decodeSqliteDatetimesAsUtc } from "./internal/sqlite-datetime.js";
|
|
23
|
+
import {
|
|
24
|
+
applySqlitePragmas,
|
|
25
|
+
assertFileBackedPath,
|
|
26
|
+
} from "./internal/sqlite-pragmas.js";
|
|
27
|
+
|
|
28
|
+
export const defaultSqlitePragmas: ReadonlyArray<string> = ["journal_mode=WAL"];
|
|
29
|
+
|
|
30
|
+
export interface SqliteDatabaseLayerOptions extends SqliteOptionsBase {
|
|
31
|
+
/** Path to the database file. In-memory databases are not supported. */
|
|
32
|
+
readonly path: string;
|
|
33
|
+
/**
|
|
34
|
+
* Pragmas applied once when the Layer connects. Defaults to
|
|
35
|
+
* {@link defaultSqlitePragmas}; pass `[]` to skip them.
|
|
36
|
+
*/
|
|
37
|
+
readonly pragmas?: ReadonlyArray<string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type SqliteFactoryOptions<Contract extends AnySqlContract> =
|
|
41
|
+
| {
|
|
42
|
+
readonly contract: Contract;
|
|
43
|
+
readonly contractJson?: never;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
readonly contractJson: unknown;
|
|
47
|
+
readonly contract?: never;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface SqliteDatabaseDefinition<
|
|
51
|
+
Contract extends AnySqlContract,
|
|
52
|
+
Requirement = ExecutorIdentifier<DefaultModels<Contract>>,
|
|
53
|
+
> extends DatabaseServiceHolder<Contract, Requirement> {
|
|
54
|
+
readonly layer: (
|
|
55
|
+
options: SqliteDatabaseLayerOptions,
|
|
56
|
+
) => Layer.Layer<DatabaseIdentifier<Contract> | Requirement, PrismaError>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const makeSqliteDatabase = <const Contract extends AnySqlContract>(
|
|
60
|
+
identifier: string,
|
|
61
|
+
options: SqliteFactoryOptions<Contract>,
|
|
62
|
+
): SqliteDatabaseDefinition<Contract> => {
|
|
63
|
+
type Models = DefaultModels<Contract>;
|
|
64
|
+
|
|
65
|
+
return makeSqlDatabase<Contract, SqliteDatabaseLayerOptions>(
|
|
66
|
+
identifier,
|
|
67
|
+
(layerOptions) => {
|
|
68
|
+
assertFileBackedPath(layerOptions.path);
|
|
69
|
+
applySqlitePragmas(
|
|
70
|
+
layerOptions.path,
|
|
71
|
+
layerOptions.pragmas ?? defaultSqlitePragmas,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const clientOptions = {
|
|
75
|
+
path: layerOptions.path,
|
|
76
|
+
extensions: layerOptions.extensions,
|
|
77
|
+
middleware: layerOptions.middleware,
|
|
78
|
+
verifyMarker: layerOptions.verifyMarker,
|
|
79
|
+
};
|
|
80
|
+
const client: SqliteClient<Contract> =
|
|
81
|
+
options.contract === undefined
|
|
82
|
+
? sqlite<Contract>({
|
|
83
|
+
...clientOptions,
|
|
84
|
+
contractJson: options.contractJson,
|
|
85
|
+
})
|
|
86
|
+
: sqlite<Contract>({
|
|
87
|
+
...clientOptions,
|
|
88
|
+
contract: options.contract,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
decodeSqliteDatetimesAsUtc(client.context);
|
|
92
|
+
|
|
93
|
+
return fromPrismaPromise(() =>
|
|
94
|
+
acquireConnectedClient(client, () => {
|
|
95
|
+
// The SQLite client already exposes the unbound namespace.
|
|
96
|
+
const models = client.orm as Models;
|
|
97
|
+
assertAvailableModelNames(Object.keys(models));
|
|
98
|
+
return {
|
|
99
|
+
client,
|
|
100
|
+
models,
|
|
101
|
+
querySemaphore: undefined,
|
|
102
|
+
transactional: false,
|
|
103
|
+
};
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
) as SqliteDatabaseDefinition<Contract>;
|
|
108
|
+
};
|