@shivaedev/effect-changes-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 +28 -0
- package/LICENSE +21 -0
- package/README.md +132 -1
- package/dist/changes.d.ts +20 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +40 -0
- package/dist/changes.js.map +1 -0
- package/dist/coverage.d.ts +20 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +16 -0
- package/dist/coverage.js.map +1 -0
- package/dist/error.d.ts +9 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +4 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +38 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +11 -0
- package/dist/model.js.map +1 -0
- package/dist/recording.d.ts +5 -0
- package/dist/recording.d.ts.map +1 -0
- package/dist/recording.js +25 -0
- package/dist/recording.js.map +1 -0
- package/dist/tables.d.ts +8 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +14 -0
- package/dist/tables.js.map +1 -0
- package/dist/transaction.d.ts +11 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +26 -0
- package/dist/transaction.js.map +1 -0
- package/dist/write.d.ts +26 -0
- package/dist/write.d.ts.map +1 -0
- package/dist/write.js +39 -0
- package/dist/write.js.map +1 -0
- package/package.json +54 -3
- package/src/changes.ts +64 -0
- package/src/coverage.ts +33 -0
- package/src/error.ts +5 -0
- package/src/index.ts +15 -0
- package/src/model.ts +47 -0
- package/src/recording.ts +40 -0
- package/src/tables.ts +23 -0
- package/src/transaction.ts +60 -0
- package/src/write.ts +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `makePrismaChanges`: bind a `@shivaedev/effect-changes` channel to a
|
|
8
|
+
Prisma Classic client with interactive `$transaction`, with the base client
|
|
9
|
+
as the channel's owner.
|
|
10
|
+
- Run `transaction(body)` inside `$transaction` with the frame opened through
|
|
11
|
+
`open` and settled from `$transaction`'s outcome: changes publish once after
|
|
12
|
+
`COMMIT`, a failing body or a failed or timed-out commit discards them, and a
|
|
13
|
+
nested `transaction` becomes a savepoint that merges on release. A caller
|
|
14
|
+
interrupted during the body rolls back; one interrupted while `COMMIT` is in
|
|
15
|
+
flight still publishes when the commit lands.
|
|
16
|
+
- Record writes automatically through `use(query)` from a typed model map,
|
|
17
|
+
`ChangeMap`, that turns each returned row into any number of changes and can
|
|
18
|
+
be made exhaustive over the client's models with `satisfies`. Writes outside a
|
|
19
|
+
transaction publish when they autocommit.
|
|
20
|
+
- Report count-only `createMany`, `updateMany` and `deleteMany` writes on mapped
|
|
21
|
+
models, and writes whose `select` or `omit` dropped a field the map reads, to
|
|
22
|
+
the `Unnamed` test seam instead of recording them.
|
|
23
|
+
- Add `recordWrite` for applications that already wrap Prisma in their own
|
|
24
|
+
Effect client.
|
|
25
|
+
- Add the coverage check: `writtenTables` reads `pg_stat_xact_user_tables` on a
|
|
26
|
+
test transaction, `tablesOf` maps tables to models from Prisma schema text,
|
|
27
|
+
and `checkCoverage` returns the tables written without a covering recorded
|
|
28
|
+
change and every unnamed write.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShivaeDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,134 @@
|
|
|
1
1
|
# @shivaedev/effect-changes-prisma
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Commit-bound change channels for Prisma Classic (`@prisma/client` with interactive `$transaction`). Writes to mapped models record their changes automatically; the changes publish once, deduplicated, after the transaction commits, and never for a rollback. A test-time coverage check reports writes that no recorded change accounts for.
|
|
4
|
+
|
|
5
|
+
It binds a [`@shivaedev/effect-changes`](../effect-changes) channel to Prisma: the channel semantics (owners, savepoint merge and discard, batches, sink failures, test seams) are the same, see [commit-bound changes](../../docs/framework/changes.md).
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { type ChangeMap, makePrismaChanges } from "@shivaedev/effect-changes-prisma";
|
|
9
|
+
import { Effect } from "effect";
|
|
10
|
+
import { PrismaClient } from "./generated/client.ts";
|
|
11
|
+
|
|
12
|
+
interface Change {
|
|
13
|
+
readonly subject: string;
|
|
14
|
+
readonly domain: string;
|
|
15
|
+
}
|
|
16
|
+
declare const prisma: PrismaClient;
|
|
17
|
+
declare const bus: { readonly emit: (change: Change) => void };
|
|
18
|
+
|
|
19
|
+
const models = {
|
|
20
|
+
Order: (order) => [{ subject: order.ownerId, domain: "orders" }],
|
|
21
|
+
Membership: (membership) => [
|
|
22
|
+
{ subject: membership.ownerId, domain: "memberships" },
|
|
23
|
+
{ subject: membership.memberId, domain: "memberships" },
|
|
24
|
+
],
|
|
25
|
+
Invoice: (invoice) => [{ subject: invoice.ownerId, domain: "invoices" }],
|
|
26
|
+
AuditNote: null,
|
|
27
|
+
} satisfies ChangeMap<PrismaClient, Change>;
|
|
28
|
+
|
|
29
|
+
const changes = makePrismaChanges({
|
|
30
|
+
name: "LiveChanges",
|
|
31
|
+
client: prisma,
|
|
32
|
+
models,
|
|
33
|
+
key: (change) => `${change.subject}:${change.domain}`,
|
|
34
|
+
publish: (batch: ReadonlyArray<Change>) => Effect.sync(() => batch.forEach((change) => bus.emit(change))),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const addMember = (id: string, ownerId: string, memberId: string) =>
|
|
38
|
+
changes.use((db) => db.membership.create({ data: { id, ownerId, memberId } })).pipe(changes.transaction);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
makePrismaChanges<Tx extends Transactional<Tx>, A, R = never>(options: PrismaChangesOptions<Tx, A, R>): PrismaChanges<Tx, A, R>
|
|
45
|
+
|
|
46
|
+
interface PrismaChangesOptions<Tx, A, R> extends Omit<ChannelOptions<A, R>, "owner" | "unowned"> {
|
|
47
|
+
readonly client: Tx & Transactional<Tx>;
|
|
48
|
+
readonly models: Partial<ChangeMap<Tx, A>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface PrismaChanges<Tx, A, R> {
|
|
52
|
+
readonly channel: Channel<A, R>;
|
|
53
|
+
readonly Client: Context.Reference<Tx>;
|
|
54
|
+
readonly use: <X>(query: (client: Tx) => PromiseLike<X>) => Effect<X, PrismaError, R>;
|
|
55
|
+
readonly transaction: <X, E, R2>(body: Effect<X, E, R2>, options?: TransactionOptions) => Effect<X, E | PrismaError, R | R2>;
|
|
56
|
+
readonly recordWrite: (write: Write) => Effect<void, never, R>;
|
|
57
|
+
readonly Unnamed: Context.Reference<(write: UnnamedWrite) => Effect<void>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type ChangeMap<Client, A> = { readonly [Model in ModelName<Client>]: ((row: ModelRow<Client, Model>, operation: RowOperation) => Iterable<A>) | null };
|
|
61
|
+
type RowOperation = "create" | "update" | "upsert" | "delete" | "createManyAndReturn" | "updateManyAndReturn";
|
|
62
|
+
type UnnamedWrite =
|
|
63
|
+
| { readonly model: string; readonly operation: string; readonly reason: "countOnly" }
|
|
64
|
+
| { readonly model: string; readonly operation: string; readonly reason: "narrowed"; readonly field: string };
|
|
65
|
+
class PrismaError { readonly _tag: "PrismaError"; readonly cause: unknown }
|
|
66
|
+
|
|
67
|
+
tablesOf(schema: string): ReadonlyMap<string, string>
|
|
68
|
+
writtenTables(client: RawQueryClient): Effect<ReadonlyArray<string>, PrismaError>
|
|
69
|
+
checkCoverage<A>(coverage: Coverage<A>): ReadonlyArray<CoverageViolation>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- `client` is the base client, usually a `PrismaClient`, or one you extended. `Tx` is inferred as the client its `$transaction` callback receives. The channel's owner is this client, so frames are kept per database.
|
|
73
|
+
- `Client` holds the client for the current scope: the base client, or the transaction client inside `transaction`. Resolve it here if your own code calls Prisma, and provide it to run code against a transaction you opened yourself.
|
|
74
|
+
- `use(query)` runs `query` against the current client and records every successful write it made to a mapped model, in the calling fiber: into the enclosing transaction's frame, into a `channel.batch`, or published at once when there is neither, because the write autocommitted. It waits for Prisma's promise, which cannot be cancelled, so a write is recorded if and only if it ran. A rejection fails with `PrismaError`.
|
|
75
|
+
- `transaction(body)` runs `body` inside `$transaction` on the current client. Inside another `transaction`, Prisma makes it a savepoint and its changes merge into the parent when it is released.
|
|
76
|
+
- `recordWrite` applies the map to a write you observed yourself, for applications that already wrap Prisma in their own Effect client.
|
|
77
|
+
- `models` maps each model name to the changes one row names, or to `null` for models whose writes publish nothing. `satisfies ChangeMap<PrismaClient, A>` makes a model added to the schema a type error until it is classified; a `Partial` map is accepted when you do not want that.
|
|
78
|
+
|
|
79
|
+
## Transactions
|
|
80
|
+
|
|
81
|
+
`transaction` opens a frame with `channel.open` in the caller's fiber, runs the body with `Effect.runPromiseExitWith` inside the `$transaction` callback, and settles the frame from the outcome of `$transaction`'s promise, not from the body:
|
|
82
|
+
|
|
83
|
+
| Situation | Result |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| The body succeeds and `$transaction` resolves | Publish once, before `transaction` returns; a savepoint merges into its parent |
|
|
86
|
+
| The body fails, dies or is interrupted | Prisma rolls back; discard; the body's failure is returned unchanged |
|
|
87
|
+
| The body succeeds but `COMMIT` fails or the transaction times out | Discard; fail with `PrismaError` |
|
|
88
|
+
| The caller is interrupted while the body runs | The body is interrupted, Prisma rolls back, nothing publishes |
|
|
89
|
+
| The caller is interrupted while `COMMIT` is in flight | Keep waiting; publish if the database committed; the caller still sees the interruption |
|
|
90
|
+
|
|
91
|
+
A frame is a root when there is no frame for the client further out, not when the current client is the base client. A test harness can therefore run each test inside a Prisma transaction that always rolls back, provide its transaction client as `Client` without opening a frame, and the application's first `transaction` inside it publishes when its savepoint is released.
|
|
92
|
+
|
|
93
|
+
## Recording
|
|
94
|
+
|
|
95
|
+
- Row-returning writes (`create`, `update`, `upsert`, `delete`, `createManyAndReturn`, `updateManyAndReturn`) pass each returned row to the model's mapping. One row may name any number of changes, for example one per subject.
|
|
96
|
+
- `createMany`, `updateMany` and `deleteMany` return only a count, so they cannot name their subjects. They record nothing and are reported to `Unnamed` as `countOnly`; the coverage check turns them into violations. Use a single-row or `*AndReturn` write on mapped models. Rows are never read back.
|
|
97
|
+
- A mapping that reads a field the write did not return, because `select` or `omit` narrowed the result, records nothing for that write; it is reported to `Unnamed` as `narrowed` with the field's name.
|
|
98
|
+
- Reads, raw queries and models mapped to `null` or absent from the map record nothing.
|
|
99
|
+
|
|
100
|
+
## Coverage check
|
|
101
|
+
|
|
102
|
+
The check runs in tests. Read the tables the test transaction wrote from `pg_stat_xact_user_tables` just before it rolls back, then compare them with the changes the channel observed:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const tables = tablesOf(await readFile("prisma/schema.prisma", "utf8"));
|
|
106
|
+
const observations: Array<Observation<Change>> = [];
|
|
107
|
+
const unnamed: Array<UnnamedWrite> = [];
|
|
108
|
+
// run the test with changes.channel.Observer and changes.Unnamed pushing into those arrays,
|
|
109
|
+
// and changes.Client set to the test's transaction client; then, before rolling back:
|
|
110
|
+
const written = yield* writtenTables(testTransaction);
|
|
111
|
+
const violations = checkCoverage({
|
|
112
|
+
written,
|
|
113
|
+
tables,
|
|
114
|
+
models,
|
|
115
|
+
observations,
|
|
116
|
+
unnamed,
|
|
117
|
+
covers: (model, change) => domains[model] === change.domain,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `tablesOf` reads the table of every model from Prisma schema text, honouring `@@map`; a model without it uses its name. Pass an explicit `Map` instead if your tables come from elsewhere.
|
|
122
|
+
- `writtenTables(client)` must run on the test's own transaction: PostgreSQL counts inserted, updated and deleted rows per transaction, including rows written in savepoints that rolled back.
|
|
123
|
+
- `checkCoverage` is a pure function and throws only if `covers` throws. It reports each written table whose model is not mapped to `null` and for which no `Recorded` observation satisfies `covers`, as `Unrecorded` (with `model: undefined` when no model owns the table), and each distinct unnamed write as `Unnamed`. `Recorded` includes changes whose frame was later discarded, matching the statistics.
|
|
124
|
+
- Raw SQL, relation writes nested in `data`, cascades and triggers write tables that nothing records; the check is how they surface. Filter the result if a table is written legitimately without a change.
|
|
125
|
+
|
|
126
|
+
## Limits
|
|
127
|
+
|
|
128
|
+
- The client handed to `use` wraps write methods of mapped models, so their results are plain promises: pass them to `transaction`, not to the array form of `$transaction`, and do not chain fluent relation calls on them. Calling `$transaction` on that client runs outside the binding; use `transaction`.
|
|
129
|
+
- A relation write nested in `data` records only the top-level model.
|
|
130
|
+
- Prisma's interactive transaction timeout (5 seconds by default) still applies; pass `{ timeout }` to `transaction`.
|
|
131
|
+
- `checkCoverage` matches tables by name, not by schema.
|
|
132
|
+
- Delivery is in-process, as for every effect-changes channel.
|
|
133
|
+
|
|
134
|
+
Tested with Prisma `7.9.1` and `@prisma/adapter-pg` on PostgreSQL 18, and Effect `4.0.0-rc.112`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Channel, type ChannelOptions } from "@shivaedev/effect-changes";
|
|
2
|
+
import { Context, Effect } from "effect";
|
|
3
|
+
import { PrismaError } from "./error.ts";
|
|
4
|
+
import type { ChangeMap, Transactional, TransactionOptions } from "./model.ts";
|
|
5
|
+
import { type UnnamedWrite, type Write } from "./write.ts";
|
|
6
|
+
export type UnnamedObserver = (write: UnnamedWrite) => Effect.Effect<void>;
|
|
7
|
+
export interface PrismaChangesOptions<Tx, A, R> extends Omit<ChannelOptions<A, R>, "owner" | "unowned"> {
|
|
8
|
+
readonly client: Tx & Transactional<Tx>;
|
|
9
|
+
readonly models: Partial<ChangeMap<Tx, A>>;
|
|
10
|
+
}
|
|
11
|
+
export interface PrismaChanges<Tx, A, R> {
|
|
12
|
+
readonly channel: Channel<A, R>;
|
|
13
|
+
readonly Client: Context.Reference<Tx>;
|
|
14
|
+
readonly use: <X>(query: (client: Tx) => PromiseLike<X>) => Effect.Effect<X, PrismaError, R>;
|
|
15
|
+
readonly transaction: <X, E, R2>(body: Effect.Effect<X, E, R2>, options?: TransactionOptions) => Effect.Effect<X, E | PrismaError, R | R2>;
|
|
16
|
+
readonly recordWrite: (write: Write) => Effect.Effect<void, never, R>;
|
|
17
|
+
readonly Unnamed: Context.Reference<UnnamedObserver>;
|
|
18
|
+
}
|
|
19
|
+
export declare const makePrismaChanges: <Tx extends Transactional<Tx>, A, R = never>(options: PrismaChangesOptions<Tx, A, R>) => PrismaChanges<Tx, A, R>;
|
|
20
|
+
//# sourceMappingURL=changes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../src/changes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAe,MAAM,2BAA2B,CAAC;AAC3F,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAG/E,OAAO,EAA4B,KAAK,YAAY,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAErF,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAE3E,MAAM,WAAW,oBAAoB,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAE,SAAQ,IAAI,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtG,QAAQ,CAAC,MAAM,EAAE,EAAE,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACvC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;IAC7F,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3I,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACtE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;CACrD;AAMD,eAAO,MAAM,iBAAiB,GAAI,EAAE,SAAS,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,oBAAoB,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,aAAa,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAmC7I,CAAC"}
|
package/dist/changes.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { makeChannel } from "@shivaedev/effect-changes";
|
|
2
|
+
import { Context, Effect } from "effect";
|
|
3
|
+
import { PrismaError } from "./error.js";
|
|
4
|
+
import { delegatesOf, recordingClient } from "./recording.js";
|
|
5
|
+
import { runTransaction } from "./transaction.js";
|
|
6
|
+
import { interpret } from "./write.js";
|
|
7
|
+
let bindings = 0;
|
|
8
|
+
const ignoreUnnamed = () => Effect.void;
|
|
9
|
+
export const makePrismaChanges = (options) => {
|
|
10
|
+
const { client, models: typed, ...channelOptions } = options;
|
|
11
|
+
const models = typed;
|
|
12
|
+
const prefix = `@shivaedev/effect-changes-prisma/${options.name}/${bindings++}`;
|
|
13
|
+
const channel = makeChannel({ ...channelOptions, owner: Effect.succeed(client) });
|
|
14
|
+
const Client = Context.Reference(`${prefix}/Client`, { defaultValue: () => client });
|
|
15
|
+
const Unnamed = Context.Reference(`${prefix}/Unnamed`, { defaultValue: () => ignoreUnnamed });
|
|
16
|
+
const delegates = delegatesOf(models);
|
|
17
|
+
const recordWrite = Effect.fn("PrismaChanges.recordWrite")(function* (write) {
|
|
18
|
+
const { changes, unnamed } = interpret(models, write);
|
|
19
|
+
if (unnamed !== undefined)
|
|
20
|
+
yield* Effect.flatMap(Effect.service(Unnamed), (observe) => observe(unnamed));
|
|
21
|
+
if (changes.length > 0)
|
|
22
|
+
yield* channel.record(changes);
|
|
23
|
+
});
|
|
24
|
+
const use = Effect.fn("PrismaChanges.use")(function* (query) {
|
|
25
|
+
const current = yield* Client;
|
|
26
|
+
const writes = [];
|
|
27
|
+
const recording = recordingClient(current, delegates, (write) => writes.push(write));
|
|
28
|
+
const exit = yield* Effect.exit(Effect.tryPromise({ try: () => query(recording), catch: (cause) => new PrismaError({ cause }) }));
|
|
29
|
+
yield* Effect.forEach(writes, recordWrite, { discard: true });
|
|
30
|
+
return yield* exit;
|
|
31
|
+
}, Effect.uninterruptible);
|
|
32
|
+
const transaction = (body, transactionOptions) => Effect.flatMap(Effect.service(Client), (current) => runTransaction({
|
|
33
|
+
client: current,
|
|
34
|
+
open: channel.open,
|
|
35
|
+
body: (tx) => Effect.provideService(body, Client, tx),
|
|
36
|
+
transaction: transactionOptions,
|
|
37
|
+
})).pipe(Effect.withSpan("PrismaChanges.transaction"));
|
|
38
|
+
return { channel, Client, use, transaction, recordWrite, Unnamed };
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=changes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes.js","sourceRoot":"","sources":["../src/changes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Observation } from "@shivaedev/effect-changes";
|
|
2
|
+
import type { UnnamedWrite } from "./write.ts";
|
|
3
|
+
export type CoverageViolation = {
|
|
4
|
+
readonly _tag: "Unrecorded";
|
|
5
|
+
readonly table: string;
|
|
6
|
+
readonly model: string | undefined;
|
|
7
|
+
} | {
|
|
8
|
+
readonly _tag: "Unnamed";
|
|
9
|
+
readonly write: UnnamedWrite;
|
|
10
|
+
};
|
|
11
|
+
export interface Coverage<A> {
|
|
12
|
+
readonly written: Iterable<string>;
|
|
13
|
+
readonly tables: ReadonlyMap<string, string>;
|
|
14
|
+
readonly models: Readonly<Record<string, unknown>>;
|
|
15
|
+
readonly observations: Iterable<Observation<A>>;
|
|
16
|
+
readonly unnamed: Iterable<UnnamedWrite>;
|
|
17
|
+
readonly covers: (model: string, change: A) => boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare const checkCoverage: <A>(coverage: Coverage<A>) => ReadonlyArray<CoverageViolation>;
|
|
20
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,MAAM,iBAAiB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC3F;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAC;AAE9D,MAAM,WAAW,QAAQ,CAAC,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;CACvD;AAQD,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,UAAU,QAAQ,CAAC,CAAC,CAAC,KAAG,aAAa,CAAC,iBAAiB,CAUvF,CAAC"}
|
package/dist/coverage.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const recordedIn = (observations) => [...observations].flatMap((observation) => (observation._tag === "Recorded" ? observation.changes : []));
|
|
2
|
+
const identify = (write) => write.reason === "narrowed" ? `${write.model}.${write.operation}.${write.field}` : `${write.model}.${write.operation}`;
|
|
3
|
+
export const checkCoverage = (coverage) => {
|
|
4
|
+
const recorded = recordedIn(coverage.observations);
|
|
5
|
+
const unrecorded = [...new Set(coverage.written)].flatMap((table) => {
|
|
6
|
+
const model = coverage.tables.get(table);
|
|
7
|
+
if (model !== undefined && coverage.models[model] === null)
|
|
8
|
+
return [];
|
|
9
|
+
if (model !== undefined && recorded.some((change) => coverage.covers(model, change)))
|
|
10
|
+
return [];
|
|
11
|
+
return [{ _tag: "Unrecorded", table, model }];
|
|
12
|
+
});
|
|
13
|
+
const unnamed = new Map([...coverage.unnamed].map((write) => [identify(write), write]));
|
|
14
|
+
return [...unrecorded, ...[...unnamed.values()].map((write) => ({ _tag: "Unnamed", write }))];
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,GAAG,CAAI,YAAsC,EAAoB,EAAE,CAClF,CAAC,GAAG,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAE1G,MAAM,QAAQ,GAAG,CAAC,KAAmB,EAAU,EAAE,CAChD,KAAK,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;AAExH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAI,QAAqB,EAAoC,EAAE;IAC3F,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAoC,EAAE;QACrG,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,KAAK,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI;YAAE,OAAO,EAAE,CAAC;QACtE,IAAI,KAAK,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC;QAChG,OAAO,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAU,CAAC,CAAC,CAAC;IACjG,OAAO,CAAC,GAAG,UAAU,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAqB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AAClH,CAAC,CAAC"}
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const PrismaError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
2
|
+
readonly _tag: "PrismaError";
|
|
3
|
+
} & Readonly<A>;
|
|
4
|
+
export declare class PrismaError extends PrismaError_base<{
|
|
5
|
+
readonly cause: unknown;
|
|
6
|
+
}> {
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":";;;AAEA,qBAAa,WAAY,SAAQ,iBAAgC;IAChE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB,CAAC;CAAG"}
|
package/dist/error.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,MAAM,OAAO,WAAY,SAAQ,IAAI,CAAC,WAAW,CAAC,aAAa,CAE7D;CAAG"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { makePrismaChanges, type PrismaChanges, type PrismaChangesOptions, type UnnamedObserver } from "./changes.ts";
|
|
2
|
+
export { type Coverage, type CoverageViolation, checkCoverage } from "./coverage.ts";
|
|
3
|
+
export { PrismaError } from "./error.ts";
|
|
4
|
+
export type { ChangeMap, CountOperation, ModelChanges, ModelName, ModelRow, RowOperation, Transactional, TransactionOptions, } from "./model.ts";
|
|
5
|
+
export { type RawQueryClient, tablesOf, writtenTables } from "./tables.ts";
|
|
6
|
+
export type { UnnamedWrite, Write } from "./write.ts";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACtH,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EACX,SAAS,EACT,cAAc,EACd,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,kBAAkB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAuE,MAAM,cAAc,CAAC;AACtH,OAAO,EAAyC,aAAa,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAWzC,OAAO,EAAuB,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface TransactionOptions {
|
|
2
|
+
readonly maxWait?: number;
|
|
3
|
+
readonly timeout?: number;
|
|
4
|
+
readonly isolationLevel?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Transactional<Tx> {
|
|
7
|
+
$transaction<X>(run: (tx: Tx) => Promise<X>, options?: TransactionOptions): PromiseLike<X>;
|
|
8
|
+
}
|
|
9
|
+
type PayloadOf<Delegate> = Delegate extends {
|
|
10
|
+
readonly [key: symbol]: {
|
|
11
|
+
readonly types: {
|
|
12
|
+
readonly payload: infer Payload;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
} ? Payload : never;
|
|
16
|
+
type Delegates<Client> = Exclude<keyof Client & string, `$${string}`>;
|
|
17
|
+
export type ModelName<Client> = {
|
|
18
|
+
[Key in Delegates<Client>]: PayloadOf<Client[Key]> extends {
|
|
19
|
+
readonly name: infer Name extends string;
|
|
20
|
+
} ? Name : never;
|
|
21
|
+
}[Delegates<Client>];
|
|
22
|
+
export type ModelRow<Client, Model extends string> = Extract<{
|
|
23
|
+
[Key in Delegates<Client>]: PayloadOf<Client[Key]> extends {
|
|
24
|
+
readonly name: Model;
|
|
25
|
+
readonly scalars: infer Row;
|
|
26
|
+
} ? Row : never;
|
|
27
|
+
}[Delegates<Client>], object>;
|
|
28
|
+
export type RowOperation = "create" | "update" | "upsert" | "delete" | "createManyAndReturn" | "updateManyAndReturn";
|
|
29
|
+
export type CountOperation = "createMany" | "updateMany" | "deleteMany";
|
|
30
|
+
export type ModelChanges<Row, A> = (row: Row, operation: RowOperation) => Iterable<A>;
|
|
31
|
+
export type ChangeMap<Client, A> = {
|
|
32
|
+
readonly [Model in ModelName<Client>]: ModelChanges<ModelRow<Client, Model>, A> | null;
|
|
33
|
+
};
|
|
34
|
+
export declare const rowOperations: ReadonlySet<string>;
|
|
35
|
+
export declare const countOperations: ReadonlySet<string>;
|
|
36
|
+
export declare const delegateOf: (model: string) => string;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,aAAa,CAAC,EAAE;IAChC,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;CAC3F;AAED,KAAK,SAAS,CAAC,QAAQ,IAAI,QAAQ,SAAS;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,KAAK,EAAE;YAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAA;SAAE,CAAA;KAAE,CAAA;CAAE,GAAG,OAAO,GAAG,KAAK,CAAC;AAElJ,KAAK,SAAS,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,MAAM,GAAG,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC,CAAC;AAEtE,MAAM,MAAM,SAAS,CAAC,MAAM,IAAI;KAC9B,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,SAAS,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,KAAK;CACtH,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;AAErB,MAAM,MAAM,QAAQ,CAAC,MAAM,EAAE,KAAK,SAAS,MAAM,IAAI,OAAO,CAC3D;KACE,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,CAAA;KAAE,GAAG,GAAG,GAAG,KAAK;CAC9H,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EACpB,MAAM,CACN,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAErH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,YAAY,GAAG,YAAY,CAAC;AAExE,MAAM,MAAM,YAAY,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEtF,MAAM,MAAM,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI;IAClC,QAAQ,EAAE,KAAK,IAAI,SAAS,CAAC,MAAM,CAAC,GAAG,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI;CACtF,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,CAO5C,CAAC;AAEH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAAuE,CAAC;AAExH,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MAA6D,CAAC"}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const rowOperations = new Set([
|
|
2
|
+
"create",
|
|
3
|
+
"update",
|
|
4
|
+
"upsert",
|
|
5
|
+
"delete",
|
|
6
|
+
"createManyAndReturn",
|
|
7
|
+
"updateManyAndReturn",
|
|
8
|
+
]);
|
|
9
|
+
export const countOperations = new Set(["createMany", "updateMany", "deleteMany"]);
|
|
10
|
+
export const delegateOf = (model) => `${model.charAt(0).toLowerCase()}${model.slice(1)}`;
|
|
11
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAmCA,MAAM,CAAC,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAe;IACvE,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,qBAAqB;IACrB,qBAAqB;CACrB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAiB,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;AAExH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LooseMap, Write } from "./write.ts";
|
|
2
|
+
export declare const isWriteOperation: (operation: string) => boolean;
|
|
3
|
+
export declare const delegatesOf: <A>(models: LooseMap<A>) => ReadonlyMap<string, string>;
|
|
4
|
+
export declare const recordingClient: <Client extends object>(client: Client, delegates: ReadonlyMap<string, string>, collect: (write: Write) => void) => Client;
|
|
5
|
+
//# sourceMappingURL=recording.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording.d.ts","sourceRoot":"","sources":["../src/recording.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAElD,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,KAAG,OAAyE,CAAC;AAE/H,eAAO,MAAM,WAAW,GAAI,CAAC,EAAE,QAAQ,QAAQ,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAK7E,CAAC;AAiBH,eAAO,MAAM,eAAe,GAAI,MAAM,SAAS,MAAM,EACpD,QAAQ,MAAM,EACd,WAAW,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,SAAS,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAC7B,MAQA,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { countOperations, delegateOf, rowOperations } from "./model.js";
|
|
2
|
+
export const isWriteOperation = (operation) => rowOperations.has(operation) || countOperations.has(operation);
|
|
3
|
+
export const delegatesOf = (models) => new Map(Object.entries(models).flatMap(([model, changesOf]) => changesOf === undefined || changesOf === null ? [] : [[delegateOf(model), model]]));
|
|
4
|
+
const bound = (value, target) => (typeof value === "function" ? value.bind(target) : value);
|
|
5
|
+
const recordingDelegate = (delegate, model, collect) => new Proxy(delegate, {
|
|
6
|
+
get: (target, key) => {
|
|
7
|
+
const value = Reflect.get(target, key);
|
|
8
|
+
if (typeof key !== "string" || typeof value !== "function" || !isWriteOperation(key))
|
|
9
|
+
return bound(value, target);
|
|
10
|
+
return (...args) => Promise.resolve(Reflect.apply(value, target, args)).then((result) => {
|
|
11
|
+
collect({ model, operation: key, result });
|
|
12
|
+
return result;
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
export const recordingClient = (client, delegates, collect) => new Proxy(client, {
|
|
17
|
+
get: (target, key) => {
|
|
18
|
+
const value = Reflect.get(target, key);
|
|
19
|
+
const model = typeof key === "string" ? delegates.get(key) : undefined;
|
|
20
|
+
if (model !== undefined && typeof value === "object" && value !== null)
|
|
21
|
+
return recordingDelegate(value, model, collect);
|
|
22
|
+
return bound(value, target);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=recording.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording.js","sourceRoot":"","sources":["../src/recording.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGxE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAiB,EAAW,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAE/H,MAAM,CAAC,MAAM,WAAW,GAAG,CAAI,MAAmB,EAA+B,EAAE,CAClF,IAAI,GAAG,CACN,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,CACrD,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,KAAK,CAAU,CAAC,CAC1F,CACD,CAAC;AAEH,MAAM,KAAK,GAAG,CAAC,KAAc,EAAE,MAAc,EAAW,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAEtH,MAAM,iBAAiB,GAAG,CAAC,QAAgB,EAAE,KAAa,EAAE,OAA+B,EAAU,EAAE,CACtG,IAAI,KAAK,CAAC,QAAQ,EAAE;IACnB,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;QACpB,MAAM,KAAK,GAAY,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,UAAU,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAClH,OAAO,CAAC,GAAG,IAA4B,EAAE,EAAE,CAC1C,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAe,EAAE,EAAE;YAC5E,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;YAC3C,OAAO,MAAM,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;CACD,CAAC,CAAC;AAEJ,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,MAAc,EACd,SAAsC,EACtC,OAA+B,EACtB,EAAE,CACX,IAAI,KAAK,CAAC,MAAM,EAAE;IACjB,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;QACpB,MAAM,KAAK,GAAY,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QACxH,OAAO,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC7B,CAAC;CACD,CAAC,CAAC"}
|
package/dist/tables.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { PrismaError } from "./error.ts";
|
|
3
|
+
export declare const tablesOf: (schema: string) => ReadonlyMap<string, string>;
|
|
4
|
+
export interface RawQueryClient {
|
|
5
|
+
$queryRawUnsafe(query: string): PromiseLike<unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare const writtenTables: (client: RawQueryClient) => Effect.Effect<string[], PrismaError, never>;
|
|
8
|
+
//# sourceMappingURL=tables.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAMzC,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,KAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAC4E,CAAC;AAEjJ,MAAM,WAAW,cAAc;IAC9B,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;CACrD;AAMD,eAAO,MAAM,aAAa,yEAIxB,CAAC"}
|
package/dist/tables.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { PrismaError } from "./error.js";
|
|
3
|
+
const MODEL = /^[ \t]*model[ \t]+(\w+)[ \t]*\{([\s\S]*?)^[ \t]*\}/gm;
|
|
4
|
+
const MAPPED = /@@map\(\s*(?:name\s*:\s*)?"([^"]+)"/;
|
|
5
|
+
const COMMENT = /\/\/.*$/gm;
|
|
6
|
+
export const tablesOf = (schema) => new Map([...schema.replace(COMMENT, "").matchAll(MODEL)].map(([, model = "", body = ""]) => [MAPPED.exec(body)?.[1] ?? model, model]));
|
|
7
|
+
const Written = Schema.Array(Schema.Struct({ relname: Schema.String }));
|
|
8
|
+
const WRITTEN = "select relname from pg_stat_xact_user_tables where n_tup_ins + n_tup_upd + n_tup_del > 0 order by relname";
|
|
9
|
+
export const writtenTables = Effect.fn("PrismaChanges.writtenTables")(function* (client) {
|
|
10
|
+
const rows = yield* Effect.tryPromise({ try: () => client.$queryRawUnsafe(WRITTEN), catch: (cause) => new PrismaError({ cause }) });
|
|
11
|
+
const decoded = yield* Effect.mapError(Schema.decodeUnknownEffect(Written)(rows), (cause) => new PrismaError({ cause }));
|
|
12
|
+
return decoded.map((row) => row.relname);
|
|
13
|
+
});
|
|
14
|
+
//# sourceMappingURL=tables.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,KAAK,GAAG,sDAAsD,CAAC;AACrE,MAAM,MAAM,GAAG,qCAAqC,CAAC;AACrD,MAAM,OAAO,GAAG,WAAW,CAAC;AAE5B,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAc,EAA+B,EAAE,CACvE,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,KAAK,CAAU,CAAC,CAAC,CAAC;AAMjJ,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAExE,MAAM,OAAO,GAAG,2GAA2G,CAAC;AAE5H,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC,6BAA6B,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAsB;IACtG,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IACpI,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACzH,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Frame } from "@shivaedev/effect-changes";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { PrismaError } from "./error.ts";
|
|
4
|
+
import type { Transactional, TransactionOptions } from "./model.ts";
|
|
5
|
+
export declare const runTransaction: <Tx extends Transactional<Tx>, X, E, R, RO>(options: {
|
|
6
|
+
readonly client: Tx;
|
|
7
|
+
readonly open: Effect.Effect<Frame, never, RO>;
|
|
8
|
+
readonly body: (tx: Tx) => Effect.Effect<X, E, R>;
|
|
9
|
+
readonly transaction: TransactionOptions | undefined;
|
|
10
|
+
}) => Effect.Effect<X, E | PrismaError, R | RO>;
|
|
11
|
+
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAgB,MAAM,EAAQ,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA6BpE,eAAO,MAAM,cAAc,GAAI,EAAE,SAAS,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,SAAS;IAClF,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,WAAW,EAAE,kBAAkB,GAAG,SAAS,CAAC;CACrD,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,GAAG,EAAE,CAsB1C,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Effect, Exit } from "effect";
|
|
2
|
+
import { PrismaError } from "./error.js";
|
|
3
|
+
class BodyFailed {
|
|
4
|
+
}
|
|
5
|
+
const commit = (client, options, body, context, signal) => {
|
|
6
|
+
let failed;
|
|
7
|
+
const run = async (tx) => {
|
|
8
|
+
const exit = await Effect.runPromiseExitWith(context)(body(tx), { signal });
|
|
9
|
+
if (Exit.isSuccess(exit))
|
|
10
|
+
return exit.value;
|
|
11
|
+
failed = exit;
|
|
12
|
+
throw new BodyFailed();
|
|
13
|
+
};
|
|
14
|
+
return Promise.resolve(client.$transaction(run, options)).then((value) => ({ committed: true, exit: Exit.succeed(value) }), (cause) => ({ committed: false, exit: failed ?? Exit.fail(new PrismaError({ cause })) }));
|
|
15
|
+
};
|
|
16
|
+
export const runTransaction = (options) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
17
|
+
const { client, body } = options;
|
|
18
|
+
const frame = yield* options.open;
|
|
19
|
+
const context = yield* Effect.context();
|
|
20
|
+
const abort = new AbortController();
|
|
21
|
+
const outcome = commit(client, options.transaction, (tx) => frame.provide(body(tx)), context, abort.signal);
|
|
22
|
+
const settle = (settled) => Effect.as(frame.settle(settled.committed ? "committed" : "rolledBack"), settled.exit);
|
|
23
|
+
const settled = yield* restore(Effect.promise(() => outcome)).pipe(Effect.onInterrupt(() => Effect.andThen(Effect.sync(() => abort.abort()), Effect.flatMap(Effect.promise(() => outcome), settle))));
|
|
24
|
+
return yield* Effect.flatten(settle(settled));
|
|
25
|
+
}));
|
|
26
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAQzC,MAAM,UAAU;CAAG;AAEnB,MAAM,MAAM,GAAG,CACd,MAAU,EACV,OAAuC,EACvC,IAAwC,EACxC,OAA2B,EAC3B,MAAmB,EACM,EAAE;IAC3B,IAAI,MAAmC,CAAC;IACxC,MAAM,GAAG,GAAG,KAAK,EAAE,EAAM,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5E,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC;QAC5C,MAAM,GAAG,IAAI,CAAC;QACd,MAAM,IAAI,UAAU,EAAE,CAAC;IACxB,CAAC,CAAC;IACF,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAC7D,CAAC,KAAK,EAAiB,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,EAC1E,CAAC,KAAc,EAAiB,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAChH,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAA4C,OAKzE,EAA6C,EAAE,CAC/C,MAAM,CAAC,mBAAmB,CAAC,CAAC,OAAO,EAAE,EAAE,CACtC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACjC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAClC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAK,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5G,MAAM,MAAM,GAAG,CAAC,OAAsB,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACjI,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CACjE,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,CACvB,MAAM,CAAC,OAAO,CACb,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAChC,MAAM,CAAC,OAAO,CACb,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,EAC7B,MAAM,CACN,CACD,CACD,CACD,CAAC;IACF,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/C,CAAC,CAAC,CACF,CAAC"}
|
package/dist/write.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type RowOperation } from "./model.ts";
|
|
2
|
+
export interface Write {
|
|
3
|
+
readonly model: string;
|
|
4
|
+
readonly operation: string;
|
|
5
|
+
readonly result: unknown;
|
|
6
|
+
}
|
|
7
|
+
export type UnnamedWrite = {
|
|
8
|
+
readonly model: string;
|
|
9
|
+
readonly operation: string;
|
|
10
|
+
readonly reason: "countOnly";
|
|
11
|
+
} | {
|
|
12
|
+
readonly model: string;
|
|
13
|
+
readonly operation: string;
|
|
14
|
+
readonly reason: "narrowed";
|
|
15
|
+
readonly field: string;
|
|
16
|
+
};
|
|
17
|
+
export type LooseChanges<A> = {
|
|
18
|
+
bivariant(row: object, operation: RowOperation): Iterable<A>;
|
|
19
|
+
}["bivariant"];
|
|
20
|
+
export type LooseMap<A> = Readonly<Record<string, LooseChanges<A> | null | undefined>>;
|
|
21
|
+
export interface Interpreted<A> {
|
|
22
|
+
readonly changes: ReadonlyArray<A>;
|
|
23
|
+
readonly unnamed: UnnamedWrite | undefined;
|
|
24
|
+
}
|
|
25
|
+
export declare const interpret: <A>(models: LooseMap<A>, write: Write) => Interpreted<A>;
|
|
26
|
+
//# sourceMappingURL=write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,YAAY,EAAiB,MAAM,YAAY,CAAC;AAE/E,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,GACrB;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GACpF;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/G,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAAE,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,WAAW,CAAC,CAAC;AAE5G,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC;AAEvF,MAAM,WAAW,WAAW,CAAC,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,CAAC;CAC3C;AAqBD,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,QAAQ,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,KAAK,KAAG,WAAW,CAAC,CAAC,CAc7E,CAAC"}
|
package/dist/write.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { countOperations, rowOperations } from "./model.js";
|
|
2
|
+
class MissingField {
|
|
3
|
+
field;
|
|
4
|
+
constructor(field) {
|
|
5
|
+
this.field = field;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const guarded = (row) => new Proxy(row, {
|
|
9
|
+
get: (target, key, receiver) => {
|
|
10
|
+
if (typeof key === "string" && !(key in target))
|
|
11
|
+
throw new MissingField(key);
|
|
12
|
+
return Reflect.get(target, key, receiver);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const isRowOperation = (operation) => rowOperations.has(operation);
|
|
16
|
+
const rowsOf = (result) => (Array.isArray(result) ? result : [result]).filter((row) => typeof row === "object" && row !== null);
|
|
17
|
+
const nothing = { changes: [], unnamed: undefined };
|
|
18
|
+
export const interpret = (models, write) => {
|
|
19
|
+
const { model, operation } = write;
|
|
20
|
+
const changesOf = models[model];
|
|
21
|
+
if (changesOf === undefined || changesOf === null)
|
|
22
|
+
return nothing;
|
|
23
|
+
if (countOperations.has(operation))
|
|
24
|
+
return { changes: [], unnamed: { model, operation, reason: "countOnly" } };
|
|
25
|
+
if (!isRowOperation(operation))
|
|
26
|
+
return nothing;
|
|
27
|
+
const changes = [];
|
|
28
|
+
try {
|
|
29
|
+
for (const row of rowsOf(write.result))
|
|
30
|
+
changes.push(...changesOf(guarded(row), operation));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof MissingField)
|
|
34
|
+
return { changes: [], unnamed: { model, operation, reason: "narrowed", field: error.field } };
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
return { changes, unnamed: undefined };
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAqB,aAAa,EAAE,MAAM,YAAY,CAAC;AAqB/E,MAAM,YAAY;IACI;IAArB,YAAqB,KAAa;QAAb,UAAK,GAAL,KAAK,CAAQ;IAAG,CAAC;CACtC;AAED,MAAM,OAAO,GAAG,CAAC,GAAW,EAAU,EAAE,CACvC,IAAI,KAAK,CAAC,GAAG,EAAE;IACd,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE;QAC9B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC;YAAE,MAAM,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7E,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC;CACD,CAAC,CAAC;AAEJ,MAAM,cAAc,GAAG,CAAC,SAAiB,EAA6B,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAEtG,MAAM,MAAM,GAAG,CAAC,MAAe,EAAyB,EAAE,CACzD,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC;AAErH,MAAM,OAAO,GAAuB,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AAExE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAI,MAAmB,EAAE,KAAY,EAAkB,EAAE;IACjF,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACnC,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC;IAClE,IAAI,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,CAAC;IAC/G,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC;QAAE,OAAO,OAAO,CAAC;IAC/C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,CAAC;QACJ,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IAC7F,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,KAAK,YAAY,YAAY;YAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QACjI,MAAM,KAAK,CAAC;IACb,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AACxC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-changes-prisma",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Commit-bound change channels for Prisma Classic: automatic change recording, publish after $transaction commits, and a test-time coverage check",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
9
10
|
"directory": "packages/effect-changes-prisma"
|
|
10
11
|
},
|
|
12
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-changes-prisma#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=24"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"README.md"
|
|
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
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
11
35
|
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@shivaedev/effect-changes": "0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@prisma/client": "^7.9.1",
|
|
44
|
+
"effect": "4.0.0-rc.112"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@prisma/adapter-pg": "7.9.1",
|
|
48
|
+
"@prisma/client": "7.9.1",
|
|
49
|
+
"@types/node": "24.10.1",
|
|
50
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
51
|
+
"effect": "4.0.0-rc.112",
|
|
52
|
+
"prisma": "7.9.1",
|
|
53
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
54
|
+
"vitest": "4.1.9"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
|
|
58
|
+
"check": "biome check .",
|
|
59
|
+
"test": "prisma generate --config test/prisma.config.ts --no-hints && vitest run",
|
|
60
|
+
"test:package": "node scripts/test-package.mjs",
|
|
61
|
+
"typecheck": "prisma generate --config test/prisma.config.ts --no-hints && tsc --noEmit",
|
|
62
|
+
"typecheck:compat": "prisma generate --config test/prisma.config.ts --no-hints && tsc6 --noEmit",
|
|
63
|
+
"ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
|
|
13
64
|
}
|
|
14
65
|
}
|
package/src/changes.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Channel, type ChannelOptions, makeChannel } from "@shivaedev/effect-changes";
|
|
2
|
+
import { Context, Effect } from "effect";
|
|
3
|
+
import { PrismaError } from "./error.ts";
|
|
4
|
+
import type { ChangeMap, Transactional, TransactionOptions } from "./model.ts";
|
|
5
|
+
import { delegatesOf, recordingClient } from "./recording.ts";
|
|
6
|
+
import { runTransaction } from "./transaction.ts";
|
|
7
|
+
import { interpret, type LooseMap, type UnnamedWrite, type Write } from "./write.ts";
|
|
8
|
+
|
|
9
|
+
export type UnnamedObserver = (write: UnnamedWrite) => Effect.Effect<void>;
|
|
10
|
+
|
|
11
|
+
export interface PrismaChangesOptions<Tx, A, R> extends Omit<ChannelOptions<A, R>, "owner" | "unowned"> {
|
|
12
|
+
readonly client: Tx & Transactional<Tx>;
|
|
13
|
+
readonly models: Partial<ChangeMap<Tx, A>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PrismaChanges<Tx, A, R> {
|
|
17
|
+
readonly channel: Channel<A, R>;
|
|
18
|
+
readonly Client: Context.Reference<Tx>;
|
|
19
|
+
readonly use: <X>(query: (client: Tx) => PromiseLike<X>) => Effect.Effect<X, PrismaError, R>;
|
|
20
|
+
readonly transaction: <X, E, R2>(body: Effect.Effect<X, E, R2>, options?: TransactionOptions) => Effect.Effect<X, E | PrismaError, R | R2>;
|
|
21
|
+
readonly recordWrite: (write: Write) => Effect.Effect<void, never, R>;
|
|
22
|
+
readonly Unnamed: Context.Reference<UnnamedObserver>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let bindings = 0;
|
|
26
|
+
|
|
27
|
+
const ignoreUnnamed: UnnamedObserver = () => Effect.void;
|
|
28
|
+
|
|
29
|
+
export const makePrismaChanges = <Tx extends Transactional<Tx>, A, R = never>(options: PrismaChangesOptions<Tx, A, R>): PrismaChanges<Tx, A, R> => {
|
|
30
|
+
const { client, models: typed, ...channelOptions } = options;
|
|
31
|
+
const models: LooseMap<A> = typed;
|
|
32
|
+
const prefix = `@shivaedev/effect-changes-prisma/${options.name}/${bindings++}`;
|
|
33
|
+
const channel = makeChannel<A, R>({ ...channelOptions, owner: Effect.succeed(client) });
|
|
34
|
+
const Client = Context.Reference<Tx>(`${prefix}/Client`, { defaultValue: () => client });
|
|
35
|
+
const Unnamed = Context.Reference<UnnamedObserver>(`${prefix}/Unnamed`, { defaultValue: () => ignoreUnnamed });
|
|
36
|
+
const delegates = delegatesOf(models);
|
|
37
|
+
|
|
38
|
+
const recordWrite = Effect.fn("PrismaChanges.recordWrite")(function* (write: Write) {
|
|
39
|
+
const { changes, unnamed } = interpret(models, write);
|
|
40
|
+
if (unnamed !== undefined) yield* Effect.flatMap(Effect.service(Unnamed), (observe) => observe(unnamed));
|
|
41
|
+
if (changes.length > 0) yield* channel.record(changes);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const use = Effect.fn("PrismaChanges.use")(function* <X>(query: (client: Tx) => PromiseLike<X>) {
|
|
45
|
+
const current = yield* Client;
|
|
46
|
+
const writes: Array<Write> = [];
|
|
47
|
+
const recording = recordingClient(current, delegates, (write) => writes.push(write));
|
|
48
|
+
const exit = yield* Effect.exit(Effect.tryPromise({ try: () => query(recording), catch: (cause) => new PrismaError({ cause }) }));
|
|
49
|
+
yield* Effect.forEach(writes, recordWrite, { discard: true });
|
|
50
|
+
return yield* exit;
|
|
51
|
+
}, Effect.uninterruptible);
|
|
52
|
+
|
|
53
|
+
const transaction = <X, E, R2>(body: Effect.Effect<X, E, R2>, transactionOptions?: TransactionOptions) =>
|
|
54
|
+
Effect.flatMap(Effect.service(Client), (current) =>
|
|
55
|
+
runTransaction({
|
|
56
|
+
client: current,
|
|
57
|
+
open: channel.open,
|
|
58
|
+
body: (tx: Tx) => Effect.provideService(body, Client, tx),
|
|
59
|
+
transaction: transactionOptions,
|
|
60
|
+
}),
|
|
61
|
+
).pipe(Effect.withSpan("PrismaChanges.transaction"));
|
|
62
|
+
|
|
63
|
+
return { channel, Client, use, transaction, recordWrite, Unnamed };
|
|
64
|
+
};
|
package/src/coverage.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Observation } from "@shivaedev/effect-changes";
|
|
2
|
+
import type { UnnamedWrite } from "./write.ts";
|
|
3
|
+
|
|
4
|
+
export type CoverageViolation =
|
|
5
|
+
| { readonly _tag: "Unrecorded"; readonly table: string; readonly model: string | undefined }
|
|
6
|
+
| { readonly _tag: "Unnamed"; readonly write: UnnamedWrite };
|
|
7
|
+
|
|
8
|
+
export interface Coverage<A> {
|
|
9
|
+
readonly written: Iterable<string>;
|
|
10
|
+
readonly tables: ReadonlyMap<string, string>;
|
|
11
|
+
readonly models: Readonly<Record<string, unknown>>;
|
|
12
|
+
readonly observations: Iterable<Observation<A>>;
|
|
13
|
+
readonly unnamed: Iterable<UnnamedWrite>;
|
|
14
|
+
readonly covers: (model: string, change: A) => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const recordedIn = <A>(observations: Iterable<Observation<A>>): ReadonlyArray<A> =>
|
|
18
|
+
[...observations].flatMap((observation) => (observation._tag === "Recorded" ? observation.changes : []));
|
|
19
|
+
|
|
20
|
+
const identify = (write: UnnamedWrite): string =>
|
|
21
|
+
write.reason === "narrowed" ? `${write.model}.${write.operation}.${write.field}` : `${write.model}.${write.operation}`;
|
|
22
|
+
|
|
23
|
+
export const checkCoverage = <A>(coverage: Coverage<A>): ReadonlyArray<CoverageViolation> => {
|
|
24
|
+
const recorded = recordedIn(coverage.observations);
|
|
25
|
+
const unrecorded = [...new Set(coverage.written)].flatMap((table): ReadonlyArray<CoverageViolation> => {
|
|
26
|
+
const model = coverage.tables.get(table);
|
|
27
|
+
if (model !== undefined && coverage.models[model] === null) return [];
|
|
28
|
+
if (model !== undefined && recorded.some((change) => coverage.covers(model, change))) return [];
|
|
29
|
+
return [{ _tag: "Unrecorded", table, model }];
|
|
30
|
+
});
|
|
31
|
+
const unnamed = new Map([...coverage.unnamed].map((write) => [identify(write), write] as const));
|
|
32
|
+
return [...unrecorded, ...[...unnamed.values()].map((write): CoverageViolation => ({ _tag: "Unnamed", write }))];
|
|
33
|
+
};
|
package/src/error.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { makePrismaChanges, type PrismaChanges, type PrismaChangesOptions, type UnnamedObserver } from "./changes.ts";
|
|
2
|
+
export { type Coverage, type CoverageViolation, checkCoverage } from "./coverage.ts";
|
|
3
|
+
export { PrismaError } from "./error.ts";
|
|
4
|
+
export type {
|
|
5
|
+
ChangeMap,
|
|
6
|
+
CountOperation,
|
|
7
|
+
ModelChanges,
|
|
8
|
+
ModelName,
|
|
9
|
+
ModelRow,
|
|
10
|
+
RowOperation,
|
|
11
|
+
Transactional,
|
|
12
|
+
TransactionOptions,
|
|
13
|
+
} from "./model.ts";
|
|
14
|
+
export { type RawQueryClient, tablesOf, writtenTables } from "./tables.ts";
|
|
15
|
+
export type { UnnamedWrite, Write } from "./write.ts";
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface TransactionOptions {
|
|
2
|
+
readonly maxWait?: number;
|
|
3
|
+
readonly timeout?: number;
|
|
4
|
+
readonly isolationLevel?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Transactional<Tx> {
|
|
8
|
+
$transaction<X>(run: (tx: Tx) => Promise<X>, options?: TransactionOptions): PromiseLike<X>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type PayloadOf<Delegate> = Delegate extends { readonly [key: symbol]: { readonly types: { readonly payload: infer Payload } } } ? Payload : never;
|
|
12
|
+
|
|
13
|
+
type Delegates<Client> = Exclude<keyof Client & string, `$${string}`>;
|
|
14
|
+
|
|
15
|
+
export type ModelName<Client> = {
|
|
16
|
+
[Key in Delegates<Client>]: PayloadOf<Client[Key]> extends { readonly name: infer Name extends string } ? Name : never;
|
|
17
|
+
}[Delegates<Client>];
|
|
18
|
+
|
|
19
|
+
export type ModelRow<Client, Model extends string> = Extract<
|
|
20
|
+
{
|
|
21
|
+
[Key in Delegates<Client>]: PayloadOf<Client[Key]> extends { readonly name: Model; readonly scalars: infer Row } ? Row : never;
|
|
22
|
+
}[Delegates<Client>],
|
|
23
|
+
object
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export type RowOperation = "create" | "update" | "upsert" | "delete" | "createManyAndReturn" | "updateManyAndReturn";
|
|
27
|
+
|
|
28
|
+
export type CountOperation = "createMany" | "updateMany" | "deleteMany";
|
|
29
|
+
|
|
30
|
+
export type ModelChanges<Row, A> = (row: Row, operation: RowOperation) => Iterable<A>;
|
|
31
|
+
|
|
32
|
+
export type ChangeMap<Client, A> = {
|
|
33
|
+
readonly [Model in ModelName<Client>]: ModelChanges<ModelRow<Client, Model>, A> | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const rowOperations: ReadonlySet<string> = new Set<RowOperation>([
|
|
37
|
+
"create",
|
|
38
|
+
"update",
|
|
39
|
+
"upsert",
|
|
40
|
+
"delete",
|
|
41
|
+
"createManyAndReturn",
|
|
42
|
+
"updateManyAndReturn",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export const countOperations: ReadonlySet<string> = new Set<CountOperation>(["createMany", "updateMany", "deleteMany"]);
|
|
46
|
+
|
|
47
|
+
export const delegateOf = (model: string): string => `${model.charAt(0).toLowerCase()}${model.slice(1)}`;
|
package/src/recording.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { countOperations, delegateOf, rowOperations } from "./model.ts";
|
|
2
|
+
import type { LooseMap, Write } from "./write.ts";
|
|
3
|
+
|
|
4
|
+
export const isWriteOperation = (operation: string): boolean => rowOperations.has(operation) || countOperations.has(operation);
|
|
5
|
+
|
|
6
|
+
export const delegatesOf = <A>(models: LooseMap<A>): ReadonlyMap<string, string> =>
|
|
7
|
+
new Map(
|
|
8
|
+
Object.entries(models).flatMap(([model, changesOf]) =>
|
|
9
|
+
changesOf === undefined || changesOf === null ? [] : [[delegateOf(model), model] as const],
|
|
10
|
+
),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const bound = (value: unknown, target: object): unknown => (typeof value === "function" ? value.bind(target) : value);
|
|
14
|
+
|
|
15
|
+
const recordingDelegate = (delegate: object, model: string, collect: (write: Write) => void): object =>
|
|
16
|
+
new Proxy(delegate, {
|
|
17
|
+
get: (target, key) => {
|
|
18
|
+
const value: unknown = Reflect.get(target, key);
|
|
19
|
+
if (typeof key !== "string" || typeof value !== "function" || !isWriteOperation(key)) return bound(value, target);
|
|
20
|
+
return (...args: ReadonlyArray<unknown>) =>
|
|
21
|
+
Promise.resolve(Reflect.apply(value, target, args)).then((result: unknown) => {
|
|
22
|
+
collect({ model, operation: key, result });
|
|
23
|
+
return result;
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const recordingClient = <Client extends object>(
|
|
29
|
+
client: Client,
|
|
30
|
+
delegates: ReadonlyMap<string, string>,
|
|
31
|
+
collect: (write: Write) => void,
|
|
32
|
+
): Client =>
|
|
33
|
+
new Proxy(client, {
|
|
34
|
+
get: (target, key) => {
|
|
35
|
+
const value: unknown = Reflect.get(target, key);
|
|
36
|
+
const model = typeof key === "string" ? delegates.get(key) : undefined;
|
|
37
|
+
if (model !== undefined && typeof value === "object" && value !== null) return recordingDelegate(value, model, collect);
|
|
38
|
+
return bound(value, target);
|
|
39
|
+
},
|
|
40
|
+
});
|
package/src/tables.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { PrismaError } from "./error.ts";
|
|
3
|
+
|
|
4
|
+
const MODEL = /^[ \t]*model[ \t]+(\w+)[ \t]*\{([\s\S]*?)^[ \t]*\}/gm;
|
|
5
|
+
const MAPPED = /@@map\(\s*(?:name\s*:\s*)?"([^"]+)"/;
|
|
6
|
+
const COMMENT = /\/\/.*$/gm;
|
|
7
|
+
|
|
8
|
+
export const tablesOf = (schema: string): ReadonlyMap<string, string> =>
|
|
9
|
+
new Map([...schema.replace(COMMENT, "").matchAll(MODEL)].map(([, model = "", body = ""]) => [MAPPED.exec(body)?.[1] ?? model, model] as const));
|
|
10
|
+
|
|
11
|
+
export interface RawQueryClient {
|
|
12
|
+
$queryRawUnsafe(query: string): PromiseLike<unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Written = Schema.Array(Schema.Struct({ relname: Schema.String }));
|
|
16
|
+
|
|
17
|
+
const WRITTEN = "select relname from pg_stat_xact_user_tables where n_tup_ins + n_tup_upd + n_tup_del > 0 order by relname";
|
|
18
|
+
|
|
19
|
+
export const writtenTables = Effect.fn("PrismaChanges.writtenTables")(function* (client: RawQueryClient) {
|
|
20
|
+
const rows = yield* Effect.tryPromise({ try: () => client.$queryRawUnsafe(WRITTEN), catch: (cause) => new PrismaError({ cause }) });
|
|
21
|
+
const decoded = yield* Effect.mapError(Schema.decodeUnknownEffect(Written)(rows), (cause) => new PrismaError({ cause }));
|
|
22
|
+
return decoded.map((row) => row.relname);
|
|
23
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Frame } from "@shivaedev/effect-changes";
|
|
2
|
+
import { type Context, Effect, Exit } from "effect";
|
|
3
|
+
import { PrismaError } from "./error.ts";
|
|
4
|
+
import type { Transactional, TransactionOptions } from "./model.ts";
|
|
5
|
+
|
|
6
|
+
interface Settled<X, E> {
|
|
7
|
+
readonly committed: boolean;
|
|
8
|
+
readonly exit: Exit.Exit<X, E | PrismaError>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class BodyFailed {}
|
|
12
|
+
|
|
13
|
+
const commit = <Tx extends Transactional<Tx>, X, E, R>(
|
|
14
|
+
client: Tx,
|
|
15
|
+
options: TransactionOptions | undefined,
|
|
16
|
+
body: (tx: Tx) => Effect.Effect<X, E, R>,
|
|
17
|
+
context: Context.Context<R>,
|
|
18
|
+
signal: AbortSignal,
|
|
19
|
+
): Promise<Settled<X, E>> => {
|
|
20
|
+
let failed: Exit.Exit<X, E> | undefined;
|
|
21
|
+
const run = async (tx: Tx) => {
|
|
22
|
+
const exit = await Effect.runPromiseExitWith(context)(body(tx), { signal });
|
|
23
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
24
|
+
failed = exit;
|
|
25
|
+
throw new BodyFailed();
|
|
26
|
+
};
|
|
27
|
+
return Promise.resolve(client.$transaction(run, options)).then(
|
|
28
|
+
(value): Settled<X, E> => ({ committed: true, exit: Exit.succeed(value) }),
|
|
29
|
+
(cause: unknown): Settled<X, E> => ({ committed: false, exit: failed ?? Exit.fail(new PrismaError({ cause })) }),
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const runTransaction = <Tx extends Transactional<Tx>, X, E, R, RO>(options: {
|
|
34
|
+
readonly client: Tx;
|
|
35
|
+
readonly open: Effect.Effect<Frame, never, RO>;
|
|
36
|
+
readonly body: (tx: Tx) => Effect.Effect<X, E, R>;
|
|
37
|
+
readonly transaction: TransactionOptions | undefined;
|
|
38
|
+
}): Effect.Effect<X, E | PrismaError, R | RO> =>
|
|
39
|
+
Effect.uninterruptibleMask((restore) =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const { client, body } = options;
|
|
42
|
+
const frame = yield* options.open;
|
|
43
|
+
const context = yield* Effect.context<R>();
|
|
44
|
+
const abort = new AbortController();
|
|
45
|
+
const outcome = commit(client, options.transaction, (tx) => frame.provide(body(tx)), context, abort.signal);
|
|
46
|
+
const settle = (settled: Settled<X, E>) => Effect.as(frame.settle(settled.committed ? "committed" : "rolledBack"), settled.exit);
|
|
47
|
+
const settled = yield* restore(Effect.promise(() => outcome)).pipe(
|
|
48
|
+
Effect.onInterrupt(() =>
|
|
49
|
+
Effect.andThen(
|
|
50
|
+
Effect.sync(() => abort.abort()),
|
|
51
|
+
Effect.flatMap(
|
|
52
|
+
Effect.promise(() => outcome),
|
|
53
|
+
settle,
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
return yield* Effect.flatten(settle(settled));
|
|
59
|
+
}),
|
|
60
|
+
);
|
package/src/write.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { countOperations, type RowOperation, rowOperations } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
export interface Write {
|
|
4
|
+
readonly model: string;
|
|
5
|
+
readonly operation: string;
|
|
6
|
+
readonly result: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type UnnamedWrite =
|
|
10
|
+
| { readonly model: string; readonly operation: string; readonly reason: "countOnly" }
|
|
11
|
+
| { readonly model: string; readonly operation: string; readonly reason: "narrowed"; readonly field: string };
|
|
12
|
+
|
|
13
|
+
export type LooseChanges<A> = { bivariant(row: object, operation: RowOperation): Iterable<A> }["bivariant"];
|
|
14
|
+
|
|
15
|
+
export type LooseMap<A> = Readonly<Record<string, LooseChanges<A> | null | undefined>>;
|
|
16
|
+
|
|
17
|
+
export interface Interpreted<A> {
|
|
18
|
+
readonly changes: ReadonlyArray<A>;
|
|
19
|
+
readonly unnamed: UnnamedWrite | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class MissingField {
|
|
23
|
+
constructor(readonly field: string) {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const guarded = (row: object): object =>
|
|
27
|
+
new Proxy(row, {
|
|
28
|
+
get: (target, key, receiver) => {
|
|
29
|
+
if (typeof key === "string" && !(key in target)) throw new MissingField(key);
|
|
30
|
+
return Reflect.get(target, key, receiver);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const isRowOperation = (operation: string): operation is RowOperation => rowOperations.has(operation);
|
|
35
|
+
|
|
36
|
+
const rowsOf = (result: unknown): ReadonlyArray<object> =>
|
|
37
|
+
(Array.isArray(result) ? result : [result]).filter((row): row is object => typeof row === "object" && row !== null);
|
|
38
|
+
|
|
39
|
+
const nothing: Interpreted<never> = { changes: [], unnamed: undefined };
|
|
40
|
+
|
|
41
|
+
export const interpret = <A>(models: LooseMap<A>, write: Write): Interpreted<A> => {
|
|
42
|
+
const { model, operation } = write;
|
|
43
|
+
const changesOf = models[model];
|
|
44
|
+
if (changesOf === undefined || changesOf === null) return nothing;
|
|
45
|
+
if (countOperations.has(operation)) return { changes: [], unnamed: { model, operation, reason: "countOnly" } };
|
|
46
|
+
if (!isRowOperation(operation)) return nothing;
|
|
47
|
+
const changes: Array<A> = [];
|
|
48
|
+
try {
|
|
49
|
+
for (const row of rowsOf(write.result)) changes.push(...changesOf(guarded(row), operation));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof MissingField) return { changes: [], unnamed: { model, operation, reason: "narrowed", field: error.field } };
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return { changes, unnamed: undefined };
|
|
55
|
+
};
|