@ddd-ts-kit/prisma 0.0.0-stage → 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/README.md +629 -2
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/outbox/locked-id-of.d.ts +2 -0
- package/dist/outbox/locked-id-of.d.ts.map +1 -0
- package/dist/outbox/locked-id-of.js +7 -0
- package/dist/outbox/locked-id-of.js.map +1 -0
- package/dist/outbox/locking-select.d.ts +4 -0
- package/dist/outbox/locking-select.d.ts.map +1 -0
- package/dist/outbox/locking-select.js +20 -0
- package/dist/outbox/locking-select.js.map +1 -0
- package/dist/outbox/payload-field.d.ts +9 -0
- package/dist/outbox/payload-field.d.ts.map +1 -0
- package/dist/outbox/payload-field.js +12 -0
- package/dist/outbox/payload-field.js.map +1 -0
- package/dist/outbox/prisma-json-input.d.ts +5 -0
- package/dist/outbox/prisma-json-input.d.ts.map +1 -0
- package/dist/outbox/prisma-json-input.js +2 -0
- package/dist/outbox/prisma-json-input.js.map +1 -0
- package/dist/outbox/prisma-outbox-claim.d.ts +25 -0
- package/dist/outbox/prisma-outbox-claim.d.ts.map +1 -0
- package/dist/outbox/prisma-outbox-claim.js +2 -0
- package/dist/outbox/prisma-outbox-claim.js.map +1 -0
- package/dist/outbox/prisma-outbox-delegate.d.ts +87 -0
- package/dist/outbox/prisma-outbox-delegate.d.ts.map +1 -0
- package/dist/outbox/prisma-outbox-delegate.js +2 -0
- package/dist/outbox/prisma-outbox-delegate.js.map +1 -0
- package/dist/outbox/prisma-outbox-store.d.ts +54 -0
- package/dist/outbox/prisma-outbox-store.d.ts.map +1 -0
- package/dist/outbox/prisma-outbox-store.js +150 -0
- package/dist/outbox/prisma-outbox-store.js.map +1 -0
- package/dist/outbox/prisma-processed-event-delegate.d.ts +17 -0
- package/dist/outbox/prisma-processed-event-delegate.d.ts.map +1 -0
- package/dist/outbox/prisma-processed-event-delegate.js +2 -0
- package/dist/outbox/prisma-processed-event-delegate.js.map +1 -0
- package/dist/outbox/prisma-processed-event-store.d.ts +19 -0
- package/dist/outbox/prisma-processed-event-store.d.ts.map +1 -0
- package/dist/outbox/prisma-processed-event-store.js +23 -0
- package/dist/outbox/prisma-processed-event-store.js.map +1 -0
- package/dist/outbox/prisma-raw-queries.d.ts +5 -0
- package/dist/outbox/prisma-raw-queries.d.ts.map +1 -0
- package/dist/outbox/prisma-raw-queries.js +2 -0
- package/dist/outbox/prisma-raw-queries.js.map +1 -0
- package/dist/outbox/to-outbox-record.d.ts +4 -0
- package/dist/outbox/to-outbox-record.d.ts.map +1 -0
- package/dist/outbox/to-outbox-record.js +25 -0
- package/dist/outbox/to-outbox-record.js.map +1 -0
- package/dist/read-model/criteria-to-prisma.d.ts +10 -0
- package/dist/read-model/criteria-to-prisma.d.ts.map +1 -0
- package/dist/read-model/criteria-to-prisma.js +23 -0
- package/dist/read-model/criteria-to-prisma.js.map +1 -0
- package/dist/read-model/predicate-to-prisma.d.ts +11 -0
- package/dist/read-model/predicate-to-prisma.d.ts.map +1 -0
- package/dist/read-model/predicate-to-prisma.js +41 -0
- package/dist/read-model/predicate-to-prisma.js.map +1 -0
- package/dist/read-model/prisma-criteria.d.ts +13 -0
- package/dist/read-model/prisma-criteria.d.ts.map +1 -0
- package/dist/read-model/prisma-criteria.js +2 -0
- package/dist/read-model/prisma-criteria.js.map +1 -0
- package/dist/read-model/prisma-model-rows.d.ts +17 -0
- package/dist/read-model/prisma-model-rows.d.ts.map +1 -0
- package/dist/read-model/prisma-model-rows.js +50 -0
- package/dist/read-model/prisma-model-rows.js.map +1 -0
- package/dist/read-model/prisma-read-model-delegate.d.ts +21 -0
- package/dist/read-model/prisma-read-model-delegate.d.ts.map +1 -0
- package/dist/read-model/prisma-read-model-delegate.js +2 -0
- package/dist/read-model/prisma-read-model-delegate.js.map +1 -0
- package/dist/read-model/prisma-read-model-options.d.ts +13 -0
- package/dist/read-model/prisma-read-model-options.d.ts.map +1 -0
- package/dist/read-model/prisma-read-model-options.js +2 -0
- package/dist/read-model/prisma-read-model-options.js.map +1 -0
- package/dist/read-model/prisma-read-model-store.d.ts +17 -0
- package/dist/read-model/prisma-read-model-store.d.ts.map +1 -0
- package/dist/read-model/prisma-read-model-store.js +13 -0
- package/dist/read-model/prisma-read-model-store.js.map +1 -0
- package/dist/read-model/prisma-read-model-table.d.ts +32 -0
- package/dist/read-model/prisma-read-model-table.d.ts.map +1 -0
- package/dist/read-model/prisma-read-model-table.js +84 -0
- package/dist/read-model/prisma-read-model-table.js.map +1 -0
- package/dist/read-model/prisma-table-rows.d.ts +27 -0
- package/dist/read-model/prisma-table-rows.d.ts.map +1 -0
- package/dist/read-model/prisma-table-rows.js +59 -0
- package/dist/read-model/prisma-table-rows.js.map +1 -0
- package/dist/read-model/row.mapper.d.ts +17 -0
- package/dist/read-model/row.mapper.d.ts.map +1 -0
- package/dist/read-model/row.mapper.js +58 -0
- package/dist/read-model/row.mapper.js.map +1 -0
- package/dist/read-model/stamped-row-of.d.ts +11 -0
- package/dist/read-model/stamped-row-of.d.ts.map +1 -0
- package/dist/read-model/stamped-row-of.js +13 -0
- package/dist/read-model/stamped-row-of.js.map +1 -0
- package/dist/read-model/written-of.d.ts +4 -0
- package/dist/read-model/written-of.d.ts.map +1 -0
- package/dist/read-model/written-of.js +12 -0
- package/dist/read-model/written-of.js.map +1 -0
- package/dist/unit-of-work/create-prisma-unit-of-work.d.ts +18 -0
- package/dist/unit-of-work/create-prisma-unit-of-work.d.ts.map +1 -0
- package/dist/unit-of-work/create-prisma-unit-of-work.js +30 -0
- package/dist/unit-of-work/create-prisma-unit-of-work.js.map +1 -0
- package/dist/unit-of-work/prisma-context.d.ts +15 -0
- package/dist/unit-of-work/prisma-context.d.ts.map +1 -0
- package/dist/unit-of-work/prisma-context.js +14 -0
- package/dist/unit-of-work/prisma-context.js.map +1 -0
- package/dist/unit-of-work/prisma-transaction-client.d.ts +8 -0
- package/dist/unit-of-work/prisma-transaction-client.d.ts.map +1 -0
- package/dist/unit-of-work/prisma-transaction-client.js +2 -0
- package/dist/unit-of-work/prisma-transaction-client.js.map +1 -0
- package/dist/unit-of-work/prisma-transaction-host.d.ts +9 -0
- package/dist/unit-of-work/prisma-transaction-host.d.ts.map +1 -0
- package/dist/unit-of-work/prisma-transaction-host.js +2 -0
- package/dist/unit-of-work/prisma-transaction-host.js.map +1 -0
- package/package.json +44 -4
- package/prisma/outbox.mysql.prisma +40 -0
- package/prisma/outbox.postgresql.prisma +43 -0
- package/prisma/outbox.sqlite.prisma +42 -0
- package/prisma/read-model.postgresql.prisma +26 -0
package/README.md
CHANGED
|
@@ -1,3 +1,630 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ddd-ts-kit/prisma
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Prisma adapters of the kernel and events ports: a unit of work that repositories never see,
|
|
4
|
+
the transactional outbox store, the processed-event store, and the models of their tables as
|
|
5
|
+
a schema fragment to copy. Same pieces as `@ddd-ts-kit/drizzle`, for projects on Prisma.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @ddd-ts-kit/prisma @prisma/client @prisma/adapter-pg
|
|
9
|
+
npm i -D prisma
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The package imports no Prisma code: it is typed structurally against the few methods it calls,
|
|
13
|
+
so it works with your generated client, wherever you generate it, and with an extended client
|
|
14
|
+
(`$extends`). PostgreSQL first; see [Dialect coverage](#dialect-coverage) for MySQL and SQLite.
|
|
15
|
+
|
|
16
|
+
## Schema
|
|
17
|
+
|
|
18
|
+
Prisma has one schema, yours. The package ships the models to copy into it, one file per
|
|
19
|
+
provider:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
node_modules/@ddd-ts-kit/prisma/prisma/outbox.postgresql.prisma
|
|
23
|
+
node_modules/@ddd-ts-kit/prisma/prisma/outbox.mysql.prisma
|
|
24
|
+
node_modules/@ddd-ts-kit/prisma/prisma/outbox.sqlite.prisma
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```prisma
|
|
28
|
+
// prisma/schema.prisma (PostgreSQL fragment)
|
|
29
|
+
model OutboxRecord {
|
|
30
|
+
id String @id
|
|
31
|
+
sequence BigInt @unique @default(autoincrement())
|
|
32
|
+
type String
|
|
33
|
+
version Int
|
|
34
|
+
occurredAt String @map("occurred_at")
|
|
35
|
+
payload Json?
|
|
36
|
+
traceparent String?
|
|
37
|
+
correlationId String? @map("correlation_id")
|
|
38
|
+
status String @default("pending")
|
|
39
|
+
attempts Int @default(0)
|
|
40
|
+
nextAttemptAt DateTime @map("next_attempt_at") @db.Timestamptz(3)
|
|
41
|
+
lastErrorCode String? @map("last_error_code")
|
|
42
|
+
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
|
|
43
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
|
44
|
+
|
|
45
|
+
@@index([status, nextAttemptAt, sequence])
|
|
46
|
+
@@map("outbox")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
model ProcessedEvent {
|
|
50
|
+
consumer String
|
|
51
|
+
eventId String @map("event_id")
|
|
52
|
+
processedAt DateTime @default(now()) @map("processed_at") @db.Timestamptz(3)
|
|
53
|
+
|
|
54
|
+
@@id([consumer, eventId])
|
|
55
|
+
@@map("processed_events")
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`prisma migrate dev` then creates the tables in a migration of your project. Keep the field
|
|
60
|
+
names, the stores are typed against them; model names, table names and column names are yours.
|
|
61
|
+
|
|
62
|
+
One schema per bounded context: list the schemas in the datasource, put each model in its own
|
|
63
|
+
with `@@schema`, and give a context that keeps its own outbox a model name of its own.
|
|
64
|
+
`schemaNameOf('billing-core')` gives the name to write, `billing_core`.
|
|
65
|
+
|
|
66
|
+
```prisma
|
|
67
|
+
datasource db {
|
|
68
|
+
provider = "postgresql"
|
|
69
|
+
schemas = ["messaging", "billing_core"]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
model MessagingOutboxRecord {
|
|
73
|
+
// ...the fields above
|
|
74
|
+
@@map("outbox")
|
|
75
|
+
@@schema("messaging")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
model Invoice {
|
|
79
|
+
id String @id
|
|
80
|
+
amount Int
|
|
81
|
+
status String
|
|
82
|
+
version Int
|
|
83
|
+
|
|
84
|
+
@@map("invoices")
|
|
85
|
+
@@schema("billing_core")
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Contexts never relate to each other's models: no `@relation` across schemas.
|
|
90
|
+
|
|
91
|
+
## Composition root
|
|
92
|
+
|
|
93
|
+
`PrismaContext` is the client repositories and stores run their queries on. It is generic over
|
|
94
|
+
the transaction client of your generated client, so the project names its own database with a
|
|
95
|
+
class, which is then the injection token:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// platform/database/database.ts
|
|
99
|
+
import { PrismaContext } from '@ddd-ts-kit/prisma';
|
|
100
|
+
import type { Prisma } from '$generated/prisma/client.js';
|
|
101
|
+
|
|
102
|
+
export abstract class Database extends PrismaContext<Prisma.TransactionClient> {}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`createPrismaUnitOfWork(prisma)` gives one object that is both the kernel `UnitOfWork` and
|
|
106
|
+
that `Database`. Bind it to both:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// platform/database/persistence.module.ts
|
|
110
|
+
import { PrismaPg } from '@prisma/adapter-pg';
|
|
111
|
+
import { defineContextModule } from '@ddd-ts-kit/di';
|
|
112
|
+
import { IntegrationEventPublisher, ProcessedEventStore, UnitOfWork } from '@ddd-ts-kit/kernel';
|
|
113
|
+
import { OutboxPublisher, OutboxStore } from '@ddd-ts-kit/events';
|
|
114
|
+
import {
|
|
115
|
+
createPrismaUnitOfWork,
|
|
116
|
+
PrismaOutboxStore,
|
|
117
|
+
PrismaProcessedEventStore,
|
|
118
|
+
} from '@ddd-ts-kit/prisma';
|
|
119
|
+
import { PrismaClient } from '$generated/prisma/client.js';
|
|
120
|
+
import { Database } from '@/platform/database/database.js';
|
|
121
|
+
|
|
122
|
+
const prisma = new PrismaClient({
|
|
123
|
+
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
|
124
|
+
});
|
|
125
|
+
const unitOfWork = createPrismaUnitOfWork(prisma);
|
|
126
|
+
const outboxStore = new PrismaOutboxStore(unitOfWork, {
|
|
127
|
+
delegate: (client) => client.messagingOutboxRecord,
|
|
128
|
+
claim: { strategy: 'skip-locked', dialect: 'postgresql', schema: 'messaging', raw: (client) => client },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const persistenceModule = defineContextModule({
|
|
132
|
+
name: 'persistence',
|
|
133
|
+
register(builder) {
|
|
134
|
+
builder.rebind(UnitOfWork).toValue(unitOfWork);
|
|
135
|
+
builder.bind(Database).toValue(unitOfWork);
|
|
136
|
+
builder.bind(OutboxStore).toValue(outboxStore);
|
|
137
|
+
builder.rebind(ProcessedEventStore).toFactory(
|
|
138
|
+
(resolve) =>
|
|
139
|
+
new PrismaProcessedEventStore(resolve(Database), { delegate: (client) => client.messagingProcessedEvent }),
|
|
140
|
+
);
|
|
141
|
+
builder.rebind(IntegrationEventPublisher).toClass(OutboxPublisher, [OutboxStore]);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// main.ts
|
|
148
|
+
const app = createApplication({ modules: [kernelModule(), persistenceModule, contextModule('billing', billingDeclarations)] });
|
|
149
|
+
|
|
150
|
+
const relay = new OutboxRelay({ store: app.resolve(OutboxStore), publisher: broker, clock: app.resolve(Clock) });
|
|
151
|
+
relay.start({ intervalMs: 500 });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
List the module after `kernelModule()`: `rebind` replaces its in-memory defaults. The command
|
|
155
|
+
bus of the kernel module runs every command in the bound `UnitOfWork`; handlers do not know it.
|
|
156
|
+
`contextModule` of `@ddd-ts-kit/di` turns the declared classes of a context into its module.
|
|
157
|
+
The stores are built here and not by `toClass`, because both need to be told how to reach
|
|
158
|
+
their model. The outbox store takes the unit of work itself, not only the context: its
|
|
159
|
+
`skip-locked` claim opens a short transaction of its own.
|
|
160
|
+
|
|
161
|
+
A project written by the CLI holds the same wiring in two files:
|
|
162
|
+
`platform/database/persistence-over.ts` builds the unit of work and the two stores once, and
|
|
163
|
+
`platform/platform.module.ts` binds them, the outbox publisher being given to
|
|
164
|
+
`kernelModule({ eventPublisher })`.
|
|
165
|
+
|
|
166
|
+
`unitOfWork.run(work)` opens the interactive `prisma.$transaction` and keeps the transaction
|
|
167
|
+
client in an `AsyncLocalStorage`; a nested `run` joins the outer transaction. `delegate` is a
|
|
168
|
+
function of the client because the model must be read on the transaction client inside a unit
|
|
169
|
+
of work and on the root client outside. Because `append` uses the current client, an event
|
|
170
|
+
published by a handler that then fails is rolled back with the business write: that is the
|
|
171
|
+
whole point of the outbox.
|
|
172
|
+
|
|
173
|
+
`createPrismaUnitOfWork(prisma, { timeout: 15_000, isolationLevel: 'RepeatableRead' })`
|
|
174
|
+
forwards the options of `$transaction`.
|
|
175
|
+
|
|
176
|
+
`run` is built on `withinUnitOfWork` of the kernel. The domain events of the aggregates saved
|
|
177
|
+
inside it wait until `work` has returned, then are dispatched inside the transaction, before
|
|
178
|
+
the commit: their handlers see every save, and what they write commits or rolls back with it.
|
|
179
|
+
If `work`, a handler or the commit fails, nothing is left: no row, no outbox record, no handler
|
|
180
|
+
run after the commit. Handlers declared `after-commit` run once the transaction has committed.
|
|
181
|
+
A nested `run` joins the unit of work in progress: one dispatch, at the end of the outermost.
|
|
182
|
+
A `run` called directly (a consumer, a script) has the same timing as a command.
|
|
183
|
+
`unitOfWorkContract` of `@ddd-ts-kit/testing/contracts` proves it, with a deferred constraint
|
|
184
|
+
for the commit that fails; on a real PostgreSQL (`DDD_TS_KIT_TEST_DATABASE_URL`) it also proves
|
|
185
|
+
that the rows of a `before-commit` handler are invisible from another connection and visible
|
|
186
|
+
to the `after-commit` handlers.
|
|
187
|
+
The `before-commit` handlers run inside the time Prisma allows the transaction (`timeout`).
|
|
188
|
+
|
|
189
|
+
## Repository
|
|
190
|
+
|
|
191
|
+
The port lives in the domain, the adapter in infrastructure. A repository receives the
|
|
192
|
+
database, never a transaction: `client()` is the transaction client of the unit of work when
|
|
193
|
+
there is one and the root client otherwise.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// contexts/billing-core/domain/repositories/invoice.repository.ts
|
|
197
|
+
import { Repository } from '@ddd-ts-kit/kernel';
|
|
198
|
+
|
|
199
|
+
export abstract class InvoiceRepository extends Repository<Invoice> {}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// contexts/billing-core/infrastructure/persistence/prisma-invoice.repository.ts
|
|
204
|
+
import { Repository } from '@ddd-ts-kit/kernel/declarations';
|
|
205
|
+
import { optimisticUpdate } from '@ddd-ts-kit/prisma';
|
|
206
|
+
|
|
207
|
+
@Repository(InvoiceRepository, [Database])
|
|
208
|
+
export class PrismaInvoiceRepository implements InvoiceRepository {
|
|
209
|
+
constructor(private readonly database: Database) {}
|
|
210
|
+
|
|
211
|
+
async findById(id: string): Promise<Invoice | undefined> {
|
|
212
|
+
const row = await this.database.client().invoice.findUnique({ where: { id } });
|
|
213
|
+
|
|
214
|
+
if (row === null) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const invoice = InvoiceMapper.toDomain(row);
|
|
219
|
+
invoice.version = row.version;
|
|
220
|
+
|
|
221
|
+
return invoice;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async save(invoice: Invoice): Promise<void> {
|
|
225
|
+
const invoices = this.database.client().invoice;
|
|
226
|
+
|
|
227
|
+
await optimisticUpdate(invoice, async ({ expected, next }) => {
|
|
228
|
+
const row = { ...invoice.state, version: next };
|
|
229
|
+
const written =
|
|
230
|
+
expected === 0
|
|
231
|
+
? await invoices.createMany({ data: [row], skipDuplicates: true })
|
|
232
|
+
: await invoices.updateMany({ where: { id: invoice.id, version: expected }, data: row });
|
|
233
|
+
|
|
234
|
+
return written.count;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
In the explicit style, without the decorator, the same binding is one line of
|
|
241
|
+
`<context>.module.ts`:
|
|
242
|
+
`builder.bind(InvoiceRepository).toClass(PrismaInvoiceRepository, [Database])`.
|
|
243
|
+
|
|
244
|
+
`Prisma.TransactionClient` is the type of `client()` for a plain client. An extended client
|
|
245
|
+
(`prisma.$extends(...)`) has a transaction client type of its own, which Prisma does not make
|
|
246
|
+
assignable to it: declare `Database` as `PrismaContext<PrismaTransactionClient<typeof prisma>>`
|
|
247
|
+
instead.
|
|
248
|
+
|
|
249
|
+
`optimisticUpdate` gives the version to expect and the version to write, raises a
|
|
250
|
+
`ConcurrencyError` (a `DomainError` with code `CONCURRENCY_CONFLICT`) when the guarded
|
|
251
|
+
statement wrote no row, and moves `invoice.version` on otherwise. Use `createMany` and
|
|
252
|
+
`updateMany` for the guarded write: they return a count, where `create` and `update` throw,
|
|
253
|
+
and a thrown unique violation aborts the surrounding PostgreSQL transaction. `upsert` cannot
|
|
254
|
+
carry the version guard.
|
|
255
|
+
|
|
256
|
+
## Audit columns, tenant scoping and pages
|
|
257
|
+
|
|
258
|
+
The standard persistence shapes of the kernel, on Prisma. A Prisma schema has no way to share
|
|
259
|
+
fields between models, so there is no helper to import here: copy the five fields. Keep their
|
|
260
|
+
names, which are those of the kernel `AuditStamp` and `TenantScoped`, so a stamp spreads into
|
|
261
|
+
`data` as it is; and keep the mapped column names (`created_at`, `created_by`, `updated_at`,
|
|
262
|
+
`updated_by`, `tenant_id`), the same in every project, which is what lets a dashboard, an
|
|
263
|
+
export or the audit package be written once.
|
|
264
|
+
|
|
265
|
+
```prisma
|
|
266
|
+
model Invoice {
|
|
267
|
+
id String @id
|
|
268
|
+
amount Int
|
|
269
|
+
status String
|
|
270
|
+
version Int
|
|
271
|
+
tenantId String @map("tenant_id")
|
|
272
|
+
createdAt DateTime @map("created_at") @db.Timestamptz(3)
|
|
273
|
+
createdBy String @map("created_by")
|
|
274
|
+
updatedAt DateTime @map("updated_at") @db.Timestamptz(3)
|
|
275
|
+
updatedBy String @map("updated_by")
|
|
276
|
+
|
|
277
|
+
@@index([tenantId, amount, id], map: "invoices_tenant_amount_idx")
|
|
278
|
+
@@map("invoices")
|
|
279
|
+
@@schema("billing_core")
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
No `@default(now())` and no `@updatedAt` on them: the instant is the one of the `Clock`, and
|
|
284
|
+
Prisma's own attributes know nothing of who acts.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { Clock, ExecutionScope, stampCreation, stampUpdate, tenantScopeOf } from '@ddd-ts-kit/kernel';
|
|
288
|
+
import { Repository } from '@ddd-ts-kit/kernel/declarations';
|
|
289
|
+
|
|
290
|
+
@Repository(InvoiceRepository, [Database, ExecutionScope, Clock])
|
|
291
|
+
export class PrismaInvoiceRepository implements InvoiceRepository {
|
|
292
|
+
constructor(
|
|
293
|
+
private readonly database: Database,
|
|
294
|
+
private readonly scope: ExecutionScope,
|
|
295
|
+
private readonly clock: Clock,
|
|
296
|
+
) {}
|
|
297
|
+
|
|
298
|
+
async findById(id: string): Promise<Invoice | undefined> {
|
|
299
|
+
// findFirst, not findUnique: the tenant is part of every condition, and it is not part of the key.
|
|
300
|
+
const row = await this.database.client().invoice.findFirst({ where: { id, ...tenantScopeOf(this.scope) } });
|
|
301
|
+
|
|
302
|
+
return row === null ? undefined : InvoiceMapper.toDomain(row);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async save(invoice: Invoice): Promise<void> {
|
|
306
|
+
await optimisticUpdate(invoice, async ({ expected, next }) => {
|
|
307
|
+
const state = { ...InvoiceMapper.toRow(invoice), version: next };
|
|
308
|
+
const written = expected === 0 ? await this.insert(state) : await this.update(state, expected);
|
|
309
|
+
|
|
310
|
+
return written.count;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private insert(state: VersionedInvoice): Promise<Prisma.BatchPayload> {
|
|
315
|
+
const row = { ...state, ...tenantScopeOf(this.scope), ...stampCreation(this.scope, this.clock) };
|
|
316
|
+
|
|
317
|
+
return this.database.client().invoice.createMany({ data: [row], skipDuplicates: true });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private update(state: VersionedInvoice, expected: number): Promise<Prisma.BatchPayload> {
|
|
321
|
+
return this.database.client().invoice.updateMany({
|
|
322
|
+
where: { id: state.id, version: expected, ...tenantScopeOf(this.scope) },
|
|
323
|
+
data: { ...state, ...stampUpdate(this.scope, this.clock) },
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
A search query takes the kernel `PageRequest` and answers a `Page`, in both variants:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
// contexts/billing-core/application/ports/invoice-queries.port.ts
|
|
333
|
+
export abstract class InvoiceQueries {
|
|
334
|
+
abstract search(request: PageRequest<'amount'>): Promise<Page<InvoiceSummary>>;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
// contexts/billing-core/infrastructure/persistence/prisma-invoice-queries.reader.ts
|
|
340
|
+
import { ExecutionScope, encodeCursor, pageOf, tenantScopeOf } from '@ddd-ts-kit/kernel';
|
|
341
|
+
import { Repository } from '@ddd-ts-kit/kernel/declarations';
|
|
342
|
+
|
|
343
|
+
@Repository(InvoiceQueries, [Database, ExecutionScope])
|
|
344
|
+
export class PrismaInvoiceQueries implements InvoiceQueries {
|
|
345
|
+
constructor(
|
|
346
|
+
private readonly database: Database,
|
|
347
|
+
private readonly scope: ExecutionScope,
|
|
348
|
+
) {}
|
|
349
|
+
|
|
350
|
+
async search(request: PageRequest<'amount'>): Promise<Page<InvoiceSummary>> {
|
|
351
|
+
return request.kind === 'offset' ? this.byOffset(request) : this.byCursor(request);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private async byOffset(request: OffsetPageRequest<'amount'>): Promise<Page<InvoiceSummary>> {
|
|
355
|
+
const { invoice } = this.database.client();
|
|
356
|
+
const where = tenantScopeOf(this.scope);
|
|
357
|
+
const rows = await invoice.findMany({
|
|
358
|
+
where,
|
|
359
|
+
orderBy: totalOrder(directionOf(request)),
|
|
360
|
+
take: request.limit,
|
|
361
|
+
skip: request.offset,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return pageOf(rows.map(toSummary), { total: await invoice.count({ where }) });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private async byCursor(request: CursorPageRequest<'amount'>): Promise<Page<InvoiceSummary>> {
|
|
368
|
+
const direction = directionOf(request);
|
|
369
|
+
const tenant = tenantScopeOf(this.scope);
|
|
370
|
+
const rows = await this.database.client().invoice.findMany({
|
|
371
|
+
where: request.cursor === undefined ? tenant : beyond(positionOf(request.cursor), direction, tenant),
|
|
372
|
+
orderBy: totalOrder(direction),
|
|
373
|
+
take: request.limit + 1,
|
|
374
|
+
});
|
|
375
|
+
const items = rows.slice(0, request.limit);
|
|
376
|
+
const last = items.at(-1);
|
|
377
|
+
const more = rows.length > request.limit && last !== undefined;
|
|
378
|
+
|
|
379
|
+
return pageOf(items.map(toSummary), more ? { nextCursor: encodeCursor({ amount: last.amount, id: last.id }) } : {});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const directionOf = (request: PageRequest<'amount'>) => request.sort[0]?.direction ?? 'asc';
|
|
384
|
+
|
|
385
|
+
// The primary key follows the sort field, in the same direction: the order is total.
|
|
386
|
+
const totalOrder = (direction: SortDirection): Prisma.InvoiceOrderByWithRelationInput[] => [
|
|
387
|
+
{ amount: direction },
|
|
388
|
+
{ id: direction },
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
// The rows after a position, as a tuple comparison: the tie-breaker decides among equals.
|
|
392
|
+
function beyond(
|
|
393
|
+
{ amount, id }: { amount: number; id: string },
|
|
394
|
+
direction: SortDirection,
|
|
395
|
+
tenant: TenantScoped,
|
|
396
|
+
): Prisma.InvoiceWhereInput {
|
|
397
|
+
const past = direction === 'asc' ? 'gt' : 'lt';
|
|
398
|
+
|
|
399
|
+
return { ...tenant, OR: [{ amount: { [past]: amount } }, { amount, id: { [past]: id } }] };
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`positionOf(cursor)` decodes with `decodeCursor` and checks that `amount` is a number and `id`
|
|
404
|
+
a string, raising `InvalidPageRequestError` otherwise. Prisma's own `cursor` option is not used:
|
|
405
|
+
it names a row by a unique field, where a position compares values and needs no row to still
|
|
406
|
+
exist.
|
|
407
|
+
|
|
408
|
+
A read adapter over the database is declared with `@Repository`, as the ones the CLI
|
|
409
|
+
generates: its kind is `repository`. Declared with `@Adapter` it would be `external`, and a
|
|
410
|
+
generated platform puts a retry and a time limit around every `external` and `gateway`
|
|
411
|
+
adapter, which a statement of the unit of work must not get.
|
|
412
|
+
|
|
413
|
+
The rules these examples hold, and that the tests of this package check on a real database:
|
|
414
|
+
|
|
415
|
+
- **Audit columns** are filled by the adapter, never by the aggregate: `stampCreation(scope,
|
|
416
|
+
clock)` on insert, `stampUpdate(scope, clock)` on update, which leaves `created_at` and
|
|
417
|
+
`created_by` alone. The actor is the one of the execution scope: the user of the request, or
|
|
418
|
+
`system:<consumer>` for a consumed event.
|
|
419
|
+
- **The tenant is part of every statement**: written on insert, and in the condition of every
|
|
420
|
+
read and every update, the version guard included. A row of another tenant is not found, and
|
|
421
|
+
an update aimed at it writes nothing, which `optimisticUpdate` reports as a
|
|
422
|
+
`ConcurrencyError`. Work that names no tenant raises `NoCurrentTenantError` before any SQL.
|
|
423
|
+
- **A list has a total order.** The primary key follows the sort field, in the same direction,
|
|
424
|
+
as a tie-breaker: rows equal on the sort field otherwise have no defined order, and a page
|
|
425
|
+
could repeat or skip one of them. The index covers `(tenant_id, <sort field>, id)`.
|
|
426
|
+
- **A cursor is a position, not a page number**: the sort value and the id of the last row,
|
|
427
|
+
written by `encodeCursor`, compared as a tuple (`amount > :amount OR (amount = :amount AND id
|
|
428
|
+
> :id)`). One row more than `limit` is fetched to know whether a page follows, so no count is
|
|
429
|
+
needed, and a row inserted before the cursor does not shift the next page as it does with an
|
|
430
|
+
offset. A cursor comes back from a client: `decodeCursor` refuses what it did not write, and
|
|
431
|
+
the adapter checks the types of the position before they reach a statement.
|
|
432
|
+
- **An offset page answers `total`**, a cursor page `nextCursor`. Offer the offset form for
|
|
433
|
+
small, browsable lists and the cursor form for anything that grows.
|
|
434
|
+
|
|
435
|
+
## Criteria
|
|
436
|
+
|
|
437
|
+
`criteriaToPrisma(plan, fields?)` turns a criteria plan of the kernel into the arguments of a
|
|
438
|
+
`findMany`: `where` (the filters, the scope of the actor and the keyset of a cursor page),
|
|
439
|
+
`counted` (the filters and the scope alone, for a `count`), `orderBy` (the total order ended
|
|
440
|
+
by the tie-breaker), `take` and `skip`. `predicateToPrisma(predicate, fields?)` gives one
|
|
441
|
+
`where`. They are data, never text: Prisma binds every value. The fields of the read model
|
|
442
|
+
are the fields of the model; `fields` renames those that differ. The text of `contains` and
|
|
443
|
+
`startsWith` has its `%`, `_` and `\` escaped, which Prisma does not do.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
// infrastructure/persistence/prisma-appointment-queries.reader.ts
|
|
447
|
+
async search(criteria: ScopedCriteria<AppointmentRow>): Promise<Page<AppointmentRow>> {
|
|
448
|
+
const plan = planCriteria(appointmentCriteria, criteria);
|
|
449
|
+
|
|
450
|
+
if (plan.empty) {
|
|
451
|
+
return pageOfPlan(plan, []);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const { where, orderBy, take, skip } = criteriaToPrisma(plan);
|
|
455
|
+
|
|
456
|
+
return pageOfPlan(plan, await this.database.client().appointmentRow.findMany({ where, orderBy, take, skip }));
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
A cursor page is fetched one row longer than asked, which is how `pageOfPlan` knows whether
|
|
461
|
+
another follows; an offset page takes its total from `count({ where: counted })`. A field
|
|
462
|
+
declared `nullable` is ordered `{ sort, nulls }`: last ascending, first descending.
|
|
463
|
+
|
|
464
|
+
**Strings.** A text column is ordered and compared by range in the collation of the column;
|
|
465
|
+
criteria order strings by code point. Declare the text columns that are sorted or compared
|
|
466
|
+
with `gt`, `lt` or `between` with `COLLATE "C"`, in the migration since the Prisma schema cannot say it (`ALTER TABLE ... ALTER COLUMN room TYPE text COLLATE "C"`): the index then serves the sort, and the
|
|
467
|
+
order is the same as in the fake. **Decimals** are a `Decimal` field and a string in the row: the store reads them back as
|
|
468
|
+
plain digits, without trailing zeros or exponent. **Instants** are `timestamptz` and a `Date`.
|
|
469
|
+
A `BigInt` field reads back as a number.
|
|
470
|
+
|
|
471
|
+
## Read models
|
|
472
|
+
|
|
473
|
+
The model of a read model is yours; `prisma/read-model.postgresql.prisma` of this package is
|
|
474
|
+
the template, with the four fields that order the events of a projection.
|
|
475
|
+
|
|
476
|
+
```prisma
|
|
477
|
+
model AppointmentRow {
|
|
478
|
+
id String @id
|
|
479
|
+
status String
|
|
480
|
+
startsAt DateTime @map("starts_at") @db.Timestamptz(3)
|
|
481
|
+
lastEventId String @map("last_event_id")
|
|
482
|
+
lastEventAt DateTime @map("last_event_at") @db.Timestamptz(3)
|
|
483
|
+
lastEventVersion BigInt? @map("last_event_version")
|
|
484
|
+
deleted Boolean @default(false)
|
|
485
|
+
|
|
486
|
+
@@map("appointment_list")
|
|
487
|
+
@@schema("scheduling")
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
// infrastructure/persistence/prisma-appointment-list.repository.ts
|
|
493
|
+
@Repository(AppointmentList, [Database])
|
|
494
|
+
export class PrismaAppointmentList extends PrismaReadModelStore<AppointmentRow, Prisma.TransactionClient> implements AppointmentList {
|
|
495
|
+
constructor(database: Database) {
|
|
496
|
+
super(database, {
|
|
497
|
+
delegate: (client) => client.appointmentRow,
|
|
498
|
+
schema: 'scheduling',
|
|
499
|
+
table: 'appointment_list',
|
|
500
|
+
columns: { id: 'id', status: 'status', startsAt: 'starts_at' },
|
|
501
|
+
definition: appointmentCriteria,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Every write goes through `TableReadModelStore` of the kernel: a row is written only if its
|
|
508
|
+
event is newer than the one it holds, on the executor of the unit of work, so the row and the
|
|
509
|
+
processed-event mark commit together. A removed row stays in the table with `deleted` set:
|
|
510
|
+
a query of your own on the table leaves those rows out, and so does a unique index
|
|
511
|
+
(`WHERE NOT deleted`). `store.table` is what `ProjectionRebuild` takes; the rebuild is
|
|
512
|
+
PostgreSQL only (a copy `<table>__rebuild` made `LIKE ... INCLUDING ALL`, the keys written
|
|
513
|
+
meanwhile in `<table>__touched`, the swap by rename in one transaction) and its guarantees
|
|
514
|
+
and limits are in the README of `@ddd-ts-kit/kernel` ("Rebuild"). The store passes
|
|
515
|
+
`criteriaContract` and `readModelStoreContract` of `@ddd-ts-kit/testing/contracts`, on PGlite
|
|
516
|
+
and on PostgreSQL 18.
|
|
517
|
+
Criteria and read models were never run on MySQL or SQLite.
|
|
518
|
+
|
|
519
|
+
The table is reached through its model, and the copy of a rebuild, which has no model,
|
|
520
|
+
through `$queryRawUnsafe` with quoted identifiers and `$n` parameters: that is why the table,
|
|
521
|
+
the schema and the column of each field are named. The arguments given to the model are built
|
|
522
|
+
from the fields of the row at run time, so `PrismaReadModelDelegate` types them as objects and
|
|
523
|
+
it is the generated client that validates them.
|
|
524
|
+
|
|
525
|
+
## Outbox and idempotency
|
|
526
|
+
|
|
527
|
+
`claim` is required, because a Prisma client does not tell which database it talks to:
|
|
528
|
+
|
|
529
|
+
- `{ strategy: 'skip-locked', dialect, raw, schema?, table? }` selects the due rows
|
|
530
|
+
`FOR UPDATE SKIP LOCKED` and leases them in the same short transaction by pushing
|
|
531
|
+
`nextAttemptAt` forward (`leaseMs`, default 30 s), so several relays share the work without
|
|
532
|
+
waiting for each other, and the records of a relay that died come back when the lease
|
|
533
|
+
expires. Needs PostgreSQL or MySQL 8. Prisma Client has no row-locking API, so this one
|
|
534
|
+
select is raw SQL sent with `$queryRawUnsafe` (`raw: (client) => client` hands the store
|
|
535
|
+
that method); `schema` and `table` are the real names of the table and must be plain
|
|
536
|
+
identifiers. Everything else, the update included, is Prisma Client.
|
|
537
|
+
- `'compare-and-set'` reads the due rows and takes each one with an `updateMany` guarded by the
|
|
538
|
+
attempt count it read: same guarantee, no raw SQL, any database, one statement per record.
|
|
539
|
+
|
|
540
|
+
`PrismaProcessedEventStore.markProcessed` is `createMany({ skipDuplicates: true })`. Run the
|
|
541
|
+
consumer inside the unit of work: the mark then commits with the effects of the handler, and
|
|
542
|
+
disappears with them if it fails.
|
|
543
|
+
|
|
544
|
+
Delivery is at least once, unordered.
|
|
545
|
+
|
|
546
|
+
## Dialect coverage
|
|
547
|
+
|
|
548
|
+
What follows is about this package. The CLI generates for PostgreSQL only: `init --orm prisma`
|
|
549
|
+
refuses `--database mysql` and `--database sqlite`.
|
|
550
|
+
|
|
551
|
+
| Database | Status |
|
|
552
|
+
|---|---|
|
|
553
|
+
| PostgreSQL | Covered by the automated suite, through `@prisma/adapter-pg`: contract suites of `@ddd-ts-kit/testing`, repository contract, rollback, nested unit of work, concurrent claims, both claim strategies. It runs on PGlite served over the PostgreSQL wire protocol by default, and on a real server when `DDD_TS_KIT_TEST_DATABASE_URL` is set (last run: PostgreSQL 18). |
|
|
554
|
+
| MySQL 8 | Not in the automated suite. The fragment is validated by `prisma validate`; the outbox and processed-event contract suites and the rollback of `append` were run once by hand against MySQL 8.4 through `@prisma/adapter-mariadb`, with `skip-locked` and `compare-and-set`. |
|
|
555
|
+
| SQLite | Not in the automated suite. The fragment is validated by `prisma validate`; the outbox contract suite was run once by hand through `@prisma/adapter-better-sqlite3` with `compare-and-set`, the only claim available there. The processed-event store needs the delegate shown under Limits. |
|
|
556
|
+
| CockroachDB, SQL Server, MongoDB | Never run. |
|
|
557
|
+
|
|
558
|
+
## Limits
|
|
559
|
+
|
|
560
|
+
What Prisma does not allow, and what the package does about it:
|
|
561
|
+
|
|
562
|
+
- **No row locking in Prisma Client.** `FOR UPDATE SKIP LOCKED` exists only as raw SQL, hence
|
|
563
|
+
the `skip-locked` option that names the dialect and the table, and the `compare-and-set`
|
|
564
|
+
alternative that stays inside Prisma Client.
|
|
565
|
+
- **Nesting is not portable.** A Prisma 7 transaction client can open a nested `$transaction`
|
|
566
|
+
(a savepoint), a Prisma 6 one cannot. The package uses neither: a nested `run` joins the
|
|
567
|
+
outer transaction, and `claimBatch` called inside a unit of work claims in that unit of
|
|
568
|
+
work.
|
|
569
|
+
- **Interactive transactions are bounded in time**: 5 seconds by default, then Prisma rolls
|
|
570
|
+
back. Pass `timeout` to `createPrismaUnitOfWork` for long commands. They also hold a
|
|
571
|
+
connection for their whole duration.
|
|
572
|
+
- **A bare `null` is refused in a `Json` field**, and the sentinels that express it live in the
|
|
573
|
+
generated client. The stores leave a `null` payload out, so it is stored as SQL `NULL` and
|
|
574
|
+
read back as `null`; the field is therefore `Json?`.
|
|
575
|
+
- **No partial index in the schema language.** The fragment declares a plain index on
|
|
576
|
+
`(status, nextAttemptAt, sequence)`. On PostgreSQL you may replace it by hand in the
|
|
577
|
+
migration with `(next_attempt_at, sequence) WHERE status = 'pending'`.
|
|
578
|
+
- **No `createMany({ skipDuplicates })` on SQLite, SQL Server and MongoDB.** There the model
|
|
579
|
+
does not fit `PrismaProcessedEventDelegate`; give the store a delegate that maps the unique
|
|
580
|
+
violation, which SQLite tolerates inside a transaction:
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
new PrismaProcessedEventStore(unitOfWork, {
|
|
584
|
+
delegate: (client) => ({
|
|
585
|
+
createMany: async ({ data }) => {
|
|
586
|
+
let count = 0;
|
|
587
|
+
for (const row of data) {
|
|
588
|
+
try {
|
|
589
|
+
await client.processedEvent.create({ data: row });
|
|
590
|
+
count += 1;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (!(error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002')) {
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return { count };
|
|
598
|
+
},
|
|
599
|
+
}),
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
- **The store cannot see the database provider**, so nothing is chosen for you: `claim` is
|
|
604
|
+
required.
|
|
605
|
+
- Tested with Prisma 7.10 and its driver adapters. The peer range also admits Prisma 6, on
|
|
606
|
+
which nothing was run.
|
|
607
|
+
- `stats()` is three `count` queries, not one statement: the numbers of a busy outbox may not
|
|
608
|
+
add up to the same instant.
|
|
609
|
+
|
|
610
|
+
## Testing an adapter
|
|
611
|
+
|
|
612
|
+
The unit of work of this package passes `unitOfWorkContract`, and its stores
|
|
613
|
+
`outboxStoreContract` and `processedEventStoreContract`, of `@ddd-ts-kit/testing/contracts`. Hold your repositories to `repositoryContract` the same way,
|
|
614
|
+
next to their in-memory fake. The tests of this package need no container and no manual step:
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
npx vitest run packages/prisma
|
|
618
|
+
DDD_TS_KIT_TEST_DATABASE_URL=postgresql://user:password@localhost:5432/postgres npx vitest run packages/prisma
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
The tests need a generated client. Each spec file first awaits
|
|
622
|
+
`test/support/generate.ts`, which builds a schema from the shipped PostgreSQL fragment (the
|
|
623
|
+
models as they are, plus a renamed copy in a `messaging` schema and the example `Invoice`),
|
|
624
|
+
runs `prisma generate` and `prisma migrate diff` into `test/generated/` (git-ignored), and
|
|
625
|
+
skips all of it when that directory is up to date. The suites themselves live in
|
|
626
|
+
`test/suites/` and import the generated client like application code would.
|
|
627
|
+
|
|
628
|
+
With the variable set, each spec file creates a database of its own on that server and drops
|
|
629
|
+
it afterwards, and the tests that need two real connections (a relay passing over rows locked
|
|
630
|
+
by another one) run instead of being skipped.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { CONCURRENCY_CONFLICT, ConcurrencyError, optimisticUpdate, schemaNameOf, type VersionStep, } from '@ddd-ts-kit/kernel';
|
|
2
|
+
export * from './unit-of-work/prisma-transaction-host.js';
|
|
3
|
+
export * from './unit-of-work/prisma-transaction-client.js';
|
|
4
|
+
export * from './unit-of-work/prisma-context.js';
|
|
5
|
+
export * from './unit-of-work/create-prisma-unit-of-work.js';
|
|
6
|
+
export * from './outbox/prisma-json-input.js';
|
|
7
|
+
export * from './outbox/prisma-outbox-delegate.js';
|
|
8
|
+
export * from './outbox/prisma-raw-queries.js';
|
|
9
|
+
export * from './outbox/prisma-outbox-claim.js';
|
|
10
|
+
export * from './outbox/prisma-outbox-store.js';
|
|
11
|
+
export * from './outbox/prisma-processed-event-delegate.js';
|
|
12
|
+
export * from './outbox/prisma-processed-event-store.js';
|
|
13
|
+
export * from './read-model/prisma-criteria.js';
|
|
14
|
+
export * from './read-model/predicate-to-prisma.js';
|
|
15
|
+
export * from './read-model/criteria-to-prisma.js';
|
|
16
|
+
export * from './read-model/prisma-read-model-delegate.js';
|
|
17
|
+
export * from './read-model/prisma-read-model-options.js';
|
|
18
|
+
export * from './read-model/prisma-read-model-table.js';
|
|
19
|
+
export * from './read-model/prisma-read-model-store.js';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAEhB,YAAY,EACZ,KAAK,WAAW,GACjB,MAAM,oBAAoB,CAAC;AAC5B,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,kCAAkC,CAAC;AACjD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,0CAA0C,CAAC;AACzD,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { CONCURRENCY_CONFLICT, ConcurrencyError, optimisticUpdate,
|
|
2
|
+
// The name to use in `@@schema("...")` and in the `schemas` list of the datasource.
|
|
3
|
+
schemaNameOf, } from '@ddd-ts-kit/kernel';
|
|
4
|
+
export * from './unit-of-work/prisma-transaction-host.js';
|
|
5
|
+
export * from './unit-of-work/prisma-transaction-client.js';
|
|
6
|
+
export * from './unit-of-work/prisma-context.js';
|
|
7
|
+
export * from './unit-of-work/create-prisma-unit-of-work.js';
|
|
8
|
+
export * from './outbox/prisma-json-input.js';
|
|
9
|
+
export * from './outbox/prisma-outbox-delegate.js';
|
|
10
|
+
export * from './outbox/prisma-raw-queries.js';
|
|
11
|
+
export * from './outbox/prisma-outbox-claim.js';
|
|
12
|
+
export * from './outbox/prisma-outbox-store.js';
|
|
13
|
+
export * from './outbox/prisma-processed-event-delegate.js';
|
|
14
|
+
export * from './outbox/prisma-processed-event-store.js';
|
|
15
|
+
export * from './read-model/prisma-criteria.js';
|
|
16
|
+
export * from './read-model/predicate-to-prisma.js';
|
|
17
|
+
export * from './read-model/criteria-to-prisma.js';
|
|
18
|
+
export * from './read-model/prisma-read-model-delegate.js';
|
|
19
|
+
export * from './read-model/prisma-read-model-options.js';
|
|
20
|
+
export * from './read-model/prisma-read-model-table.js';
|
|
21
|
+
export * from './read-model/prisma-read-model-store.js';
|
|
22
|
+
//# sourceMappingURL=index.js.map
|