@stratal/framework 0.0.26 → 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 +596 -0
- package/README.md +166 -24
- package/dist/access-control/index.d.mts +43 -16
- package/dist/access-control/index.d.mts.map +1 -1
- package/dist/access-control/index.mjs +5 -5
- package/dist/access-control/index.mjs.map +1 -1
- package/dist/{access.service-BmDhE-re.mjs → access.service-BjmnWBEo.mjs} +35 -17
- package/dist/access.service-BjmnWBEo.mjs.map +1 -0
- package/dist/auth/index.d.mts +105 -103
- package/dist/auth/index.d.mts.map +1 -1
- package/dist/auth/index.mjs +123 -24
- package/dist/auth/index.mjs.map +1 -1
- package/dist/{auth-context-CGVbiSX3.d.mts → auth-context-C1om3Zsr.d.mts} +1 -2
- package/dist/auth-context-C1om3Zsr.d.mts.map +1 -0
- package/dist/{auth-context-C8NBfiMa.mjs → auth-context-cNSS1rmh.mjs} +2 -2
- package/dist/{auth-context-C8NBfiMa.mjs.map → auth-context-cNSS1rmh.mjs.map} +1 -1
- package/dist/auth.service-Onf3JkyL.d.mts +44 -0
- package/dist/auth.service-Onf3JkyL.d.mts.map +1 -0
- package/dist/context/index.d.mts +4 -5
- package/dist/context/index.d.mts.map +1 -1
- package/dist/context/index.mjs +1 -1
- package/dist/database/index.d.mts +3 -3
- package/dist/database/index.mjs +413 -34
- package/dist/database/index.mjs.map +1 -1
- package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/{decorateParam-DwV9LSPl.mjs → decorateParam-xwTkq9gO.mjs} +2 -2
- package/dist/{decorateParam-DwV9LSPl.mjs.map → decorateParam-xwTkq9gO.mjs.map} +1 -1
- package/dist/factory/index.d.mts +3 -5
- package/dist/factory/index.d.mts.map +1 -1
- package/dist/factory/index.mjs.map +1 -1
- package/dist/guards/index.d.mts +3 -4
- package/dist/guards/index.d.mts.map +1 -1
- package/dist/guards/index.mjs +4 -4
- package/dist/guards/index.mjs.map +1 -1
- package/dist/index-e_u1SRyd.d.mts +921 -0
- package/dist/index-e_u1SRyd.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/{types-CWZ9q74G.d.mts → types-B35g-lXi.d.mts} +18 -4
- package/dist/types-B35g-lXi.d.mts.map +1 -0
- package/package.json +31 -27
- package/dist/access.service-BmDhE-re.mjs.map +0 -1
- package/dist/auth-context-CGVbiSX3.d.mts.map +0 -1
- package/dist/index-Dt0YUA7r.d.mts +0 -446
- package/dist/index-Dt0YUA7r.d.mts.map +0 -1
- package/dist/types-CWZ9q74G.d.mts.map +0 -1
- package/dist/types-DabF8LGz.d.mts +0 -11
- package/dist/types-DabF8LGz.d.mts.map +0 -1
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { a as InferAnySchema, n as DeclaredMembers, o as InferConnectionExtensions, r as DefaultConnectionName, s as InferConnectionSchema, t as ConnectionName } from "./types-B35g-lXi.mjs";
|
|
2
|
+
import { AsyncModuleOptions, DynamicModule, ModuleContext, OnInitialize, OnShutdown } from "stratal/module";
|
|
3
|
+
import { ApplicationError, HttpException } from "stratal/errors";
|
|
4
|
+
import { Command } from "stratal/quarry";
|
|
5
|
+
import { AggregateArgs, AllCrudOperations, AnyPlugin, ClientContract, ClientOptions, ComputedFieldsOptions, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, EntityMutationHooksDef, FindFirstArgs, FindManyArgs, FindUniqueArgs, GroupByArgs, ModelResult, QueryOptions, RuntimePlugin, SelectSubset, SimplifiedPlainResult, TransactionClientContract, TransactionIsolationLevel, UpdateArgs, UpdateManyArgs, UpsertArgs } from "@zenstackhq/orm";
|
|
6
|
+
import { SchemaDef } from "@zenstackhq/schema";
|
|
7
|
+
import { GetModels, SchemaDef as SchemaDef$1 } from "@zenstackhq/orm/schema";
|
|
8
|
+
import { IEventRegistry } from "stratal/events";
|
|
9
|
+
//#region src/database/database.module.d.ts
|
|
10
|
+
interface DatabaseConnectionConfig<Schema extends SchemaDef = SchemaDef, Name extends ConnectionName = ConnectionName> {
|
|
11
|
+
name: Name;
|
|
12
|
+
schema: Schema;
|
|
13
|
+
dialect: () => ClientOptions<SchemaDef>['dialect'];
|
|
14
|
+
plugins?: AnyPlugin[];
|
|
15
|
+
/**
|
|
16
|
+
* Schema-level @computed field implementations. Required when the schema
|
|
17
|
+
* declares any `@computed` fields. Keyed by uncapitalized model name; values
|
|
18
|
+
* map field name to a Kysely-expression compute callback.
|
|
19
|
+
*/
|
|
20
|
+
computedFields?: ComputedFieldsOptions<Schema>;
|
|
21
|
+
}
|
|
22
|
+
interface DatabaseModuleConfig {
|
|
23
|
+
default: DefaultConnectionName;
|
|
24
|
+
connections: DatabaseConnectionConfig[];
|
|
25
|
+
}
|
|
26
|
+
declare class DatabaseModule implements OnInitialize, OnShutdown {
|
|
27
|
+
private readonly services;
|
|
28
|
+
static forRoot(config: DatabaseModuleConfig): DynamicModule;
|
|
29
|
+
static forRootAsync(options: AsyncModuleOptions<DatabaseModuleConfig>): DynamicModule;
|
|
30
|
+
onInitialize(context: ModuleContext): Promise<void>;
|
|
31
|
+
onShutdown(context: ModuleContext): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/database/pagination/cursor.d.ts
|
|
35
|
+
/**
|
|
36
|
+
* Reading rows from a position in an ordering.
|
|
37
|
+
*
|
|
38
|
+
* A primitive on its own terms, not a variant of anything: it answers "the N
|
|
39
|
+
* rows after this row", and that is the whole contract. There are no page
|
|
40
|
+
* numbers, no total, and no last page, because a cursor names a row rather than
|
|
41
|
+
* an offset into a result set. It requires an ordering — a total one — which is
|
|
42
|
+
* the price of addressing a row at all.
|
|
43
|
+
*
|
|
44
|
+
* What that buys: rows inserted or deleted around the reader do not move the
|
|
45
|
+
* position. `LIMIT/OFFSET` addresses a place in a result set, so a row added
|
|
46
|
+
* above the window shifts everything under it and the reader silently skips one
|
|
47
|
+
* or sees one twice. Nothing here shifts.
|
|
48
|
+
*
|
|
49
|
+
* Knows nothing of HTTP or Inertia. A route can return the result as JSON, a
|
|
50
|
+
* client can walk it, and `ctx.scroll()` can read it — none of which this file
|
|
51
|
+
* is aware of.
|
|
52
|
+
*/
|
|
53
|
+
/** Sort direction for one ordering column. */
|
|
54
|
+
type CursorSortOrder = 'asc' | 'desc';
|
|
55
|
+
/**
|
|
56
|
+
* One ordering column. Several are combined left to right, as in SQL.
|
|
57
|
+
*
|
|
58
|
+
* Values may be `undefined` so an array of clause literals keeps its ordinary
|
|
59
|
+
* inferred type — TypeScript widens `[{ updatedAt: 'desc' }, { id: 'desc' }]`
|
|
60
|
+
* to a union whose members carry the other's key as `undefined`. A direction
|
|
61
|
+
* that really is `undefined` at runtime still raises `CursorOrderingError`.
|
|
62
|
+
*/
|
|
63
|
+
type CursorOrderBy = Record<string, CursorSortOrder | undefined>;
|
|
64
|
+
interface CursorPageArgs {
|
|
65
|
+
/**
|
|
66
|
+
* The cursor to read from, or `null` to start at the beginning of the
|
|
67
|
+
* ordering. Opaque: it is minted by a previous result and passed back
|
|
68
|
+
* verbatim. Never construct one.
|
|
69
|
+
*/
|
|
70
|
+
cursor?: string | null;
|
|
71
|
+
/** Rows per page. The last page may hold fewer. */
|
|
72
|
+
take: number;
|
|
73
|
+
/**
|
|
74
|
+
* Ordering, applied left to right. Required — a cursor addresses a row's
|
|
75
|
+
* position in an ordering, so without one there is no position to address.
|
|
76
|
+
* The set must include {@link CursorPageArgs.uniqueBy}.
|
|
77
|
+
*/
|
|
78
|
+
orderBy: CursorOrderBy | CursorOrderBy[];
|
|
79
|
+
/**
|
|
80
|
+
* The ordering column that makes the ordering total. Defaults to `id`.
|
|
81
|
+
*
|
|
82
|
+
* Ordering on a non-unique column alone leaves ties, and a cursor cannot
|
|
83
|
+
* address a position inside a tie — which reintroduces exactly the skipping
|
|
84
|
+
* that cursors exist to prevent. `updatedAt desc` needs an `id desc` after it.
|
|
85
|
+
*/
|
|
86
|
+
uniqueBy?: string;
|
|
87
|
+
/** Filter, combined with the cursor's own condition. */
|
|
88
|
+
where?: Record<string, unknown>;
|
|
89
|
+
/**
|
|
90
|
+
* Relations to load, field selection, and fields to drop — handed to the
|
|
91
|
+
* delegate untouched. `select` and `omit` must not drop an ordering column,
|
|
92
|
+
* because the cursor is built from those.
|
|
93
|
+
*
|
|
94
|
+
* Deliberately not mutually exclusive here, unlike on `db.$cursor`, because
|
|
95
|
+
* the delegate decides what they mean: a model delegate already refuses the
|
|
96
|
+
* combination itself (`"select" and "omit" cannot be used together`), and a
|
|
97
|
+
* hand-written one owns its `findMany` and may honour any combination.
|
|
98
|
+
*/
|
|
99
|
+
include?: Record<string, unknown>;
|
|
100
|
+
select?: Record<string, unknown>;
|
|
101
|
+
omit?: Record<string, unknown>;
|
|
102
|
+
/** Query parameter name a caller should send the cursor under. Defaults to `cursor`. */
|
|
103
|
+
cursorName?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One page of rows read by cursor.
|
|
107
|
+
*
|
|
108
|
+
* Carries no `path` or page URLs, unlike Laravel's `CursorPaginator`. Building
|
|
109
|
+
* a URL is a routing decision — which route, which of the current query
|
|
110
|
+
* parameters to keep, whether a trailing slash is canonical — and a paginator
|
|
111
|
+
* holds none of that. A route that wants links builds them from its own route
|
|
112
|
+
* helper plus `nextCursor` / `prevCursor`.
|
|
113
|
+
*/
|
|
114
|
+
interface CursorPageResult<TRow> {
|
|
115
|
+
/** The rows for this page, in display order. */
|
|
116
|
+
data: TRow[];
|
|
117
|
+
/** Rows requested per page. The last page may hold fewer. */
|
|
118
|
+
perPage: number;
|
|
119
|
+
/** Query parameter name the cursor travels under. */
|
|
120
|
+
cursorName: string;
|
|
121
|
+
/**
|
|
122
|
+
* The cursor this page was read from, or `null` on the first page.
|
|
123
|
+
*
|
|
124
|
+
* Laravel's `CursorPaginator::toArray()` omits both this and the cursor name
|
|
125
|
+
* because it recovers them from the request. A result object has no request,
|
|
126
|
+
* so it carries them.
|
|
127
|
+
*/
|
|
128
|
+
cursor: string | null;
|
|
129
|
+
/** Cursor for the following page, or `null` when this is the last. */
|
|
130
|
+
nextCursor: string | null;
|
|
131
|
+
/** Cursor for the preceding page, or `null` when this is the first. */
|
|
132
|
+
prevCursor: string | null;
|
|
133
|
+
}
|
|
134
|
+
/** Whether a cursor points forward (the next rows) or backward (the previous ones). */
|
|
135
|
+
type CursorDirection = 'next' | 'prev';
|
|
136
|
+
interface DecodedCursor {
|
|
137
|
+
/** The ordering column values of the row the cursor addresses. */
|
|
138
|
+
values: Record<string, unknown>;
|
|
139
|
+
direction: CursorDirection;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The query {@link readCursorPage} builds — the single declaration of it, used
|
|
143
|
+
* both as the delegate's parameter type and as the type of the object handed to
|
|
144
|
+
* `findMany`.
|
|
145
|
+
*
|
|
146
|
+
* Every key here must also be a key of a ZenStack `findMany` arg, because that
|
|
147
|
+
* is what makes a model delegate satisfy {@link CursorPageDelegate}: ZenStack
|
|
148
|
+
* types `findMany` as `<T extends FindManyArgs>(args?: SelectSubset<T, …>)`,
|
|
149
|
+
* and `SelectSubset` maps each key of `T` to `never` unless the model's own
|
|
150
|
+
* args declare it. A widened stand-in — `Record<string, unknown>` — infers `T`
|
|
151
|
+
* to itself, collapses the whole parameter to `{ [x: string]: never }`, and no
|
|
152
|
+
* delegate can satisfy it in either direction.
|
|
153
|
+
*/
|
|
154
|
+
interface CursorFindManyArgs {
|
|
155
|
+
where?: Record<string, unknown>;
|
|
156
|
+
orderBy?: CursorOrderBy[];
|
|
157
|
+
take?: number;
|
|
158
|
+
include?: Record<string, unknown>;
|
|
159
|
+
select?: Record<string, unknown>;
|
|
160
|
+
omit?: Record<string, unknown>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The minimum a model delegate has to offer.
|
|
164
|
+
*
|
|
165
|
+
* A ZenStack model delegate satisfies this structurally — `db.thread` is passed
|
|
166
|
+
* as-is — and so does a hand-written `findMany`, which is how a query the ORM
|
|
167
|
+
* cannot express (a `UNION`, a raw statement) is paged by the same primitive.
|
|
168
|
+
*/
|
|
169
|
+
interface CursorPageDelegate<TRow> {
|
|
170
|
+
findMany(args: CursorFindManyArgs): PromiseLike<TRow[]>;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Encodes the ordering values of a row plus the direction it points.
|
|
174
|
+
*
|
|
175
|
+
* The whole ordering tuple travels, not a row id, so the cursor still resolves
|
|
176
|
+
* after the row it was built from is deleted — which is the case cursors are
|
|
177
|
+
* for. The direction rides along because a caller sends every cursor under one
|
|
178
|
+
* query parameter name and nothing else distinguishes forward from backward.
|
|
179
|
+
*/
|
|
180
|
+
declare function encodeCursor(values: Record<string, unknown>, direction: CursorDirection): string;
|
|
181
|
+
/** Reads a cursor minted by {@link encodeCursor}. Throws on anything else. */
|
|
182
|
+
declare function decodeCursor(cursor: string): DecodedCursor;
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/database/pagination/cursor-reader.d.ts
|
|
185
|
+
/**
|
|
186
|
+
* Per-model cursor reading: `db.$cursor.thread.findMany({ … })`.
|
|
187
|
+
*
|
|
188
|
+
* A namespace of its own rather than an argument on the model's own `findMany`,
|
|
189
|
+
* because ZenStack already has a `cursor` there and it means something else. Its
|
|
190
|
+
* `cursor` takes a `WhereUniqueInput` and resolves the row's ordering values by
|
|
191
|
+
* subquery at query time, so the row must still exist, still pass the query's
|
|
192
|
+
* own `where`, and still hold the ordering values it held when the cursor was
|
|
193
|
+
* minted. Two arguments spelled `cursor` on one call, one of which answers an
|
|
194
|
+
* empty page when its row is deleted, is not a distinction a caller should have
|
|
195
|
+
* to hold. In here there is exactly one: {@link CursorReaderArgs} omits
|
|
196
|
+
* ZenStack's `cursor` and the `skip` that goes with it.
|
|
197
|
+
*/
|
|
198
|
+
/**
|
|
199
|
+
* The query-relevant options the result and argument types read.
|
|
200
|
+
*
|
|
201
|
+
* `QueryOptions` is what ZenStack's own `SimplifiedPlainResult` and `FindManyArgs` default this
|
|
202
|
+
* slot to. Narrowing `ClientOptions` to the keys they share yields that same type, but reaches
|
|
203
|
+
* it through `keyof ClientOptions`, which instantiates the client-only `computedFields` map
|
|
204
|
+
* across every model and field in the schema for a result that never reads it.
|
|
205
|
+
*/
|
|
206
|
+
type Options<Schema extends SchemaDef$1> = QueryOptions<Schema>;
|
|
207
|
+
/** The client keys that address a model — `Post` in the schema is `post` here. */
|
|
208
|
+
type CursorModelKey<Schema extends SchemaDef$1> = Uncapitalize<GetModels<Schema>>;
|
|
209
|
+
/** The schema model a client key addresses. */
|
|
210
|
+
type ModelOf<Schema extends SchemaDef$1, Key extends CursorModelKey<Schema>> = Extract<GetModels<Schema>, { [M in GetModels<Schema>]: Uncapitalize<M> extends Key ? M : never; }[GetModels<Schema>]>;
|
|
211
|
+
/** One row of a model, before `select`/`include` narrow it. */
|
|
212
|
+
type PlainRow<Schema extends SchemaDef$1, Key extends CursorModelKey<Schema>> = SimplifiedPlainResult<Schema, ModelOf<Schema, Key>, {}, Options<Schema>>;
|
|
213
|
+
/**
|
|
214
|
+
* The ordering, restricted to the model's own scalar columns.
|
|
215
|
+
*
|
|
216
|
+
* Narrower than ZenStack's `orderBy` on purpose: a cursor is built from the
|
|
217
|
+
* ordering values of a row, so an ordering that reaches through a relation or
|
|
218
|
+
* sorts on a computed relevance score has no value to carry. Those are
|
|
219
|
+
* unrepresentable here rather than accepted and failed on at runtime.
|
|
220
|
+
*/
|
|
221
|
+
type CursorOrderByOf<Schema extends SchemaDef$1, Key extends CursorModelKey<Schema>> = Partial<Record<keyof PlainRow<Schema, Key> & string, CursorSortOrder>>;
|
|
222
|
+
/**
|
|
223
|
+
* A model's `findMany` arguments, with cursor paging in place of offset paging.
|
|
224
|
+
*
|
|
225
|
+
* `skip` goes with ZenStack's `cursor` and addresses a place in a result set,
|
|
226
|
+
* which is the thing a cursor exists not to do. `distinct` changes which row
|
|
227
|
+
* represents a group, so no row has a stable position to address.
|
|
228
|
+
*
|
|
229
|
+
* Dropping an argument here only stops it being declared; what refuses it at a
|
|
230
|
+
* call site is {@link CursorModelReader.findMany} passing this through
|
|
231
|
+
* ZenStack's `SelectSubset`.
|
|
232
|
+
*/
|
|
233
|
+
type CursorReaderArgs<Schema extends SchemaDef$1, Key extends CursorModelKey<Schema>> = Omit<FindManyArgs<Schema, ModelOf<Schema, Key>, Options<Schema>>, 'cursor' | 'skip' | 'take' | 'orderBy' | 'distinct'> & {
|
|
234
|
+
/**
|
|
235
|
+
* The cursor to read from, or `null`/absent to start at the beginning of
|
|
236
|
+
* the ordering. Opaque: it is minted by a previous result and passed back
|
|
237
|
+
* verbatim. Never construct one.
|
|
238
|
+
*/
|
|
239
|
+
cursor?: string | null;
|
|
240
|
+
/** Rows per page. The last page may hold fewer. */
|
|
241
|
+
take: number;
|
|
242
|
+
/**
|
|
243
|
+
* Ordering, applied left to right. Required, and must end in a unique
|
|
244
|
+
* column — a cursor addresses a row's position in an ordering, and a
|
|
245
|
+
* position inside a tie cannot be addressed.
|
|
246
|
+
*/
|
|
247
|
+
orderBy: CursorOrderByOf<Schema, Key> | CursorOrderByOf<Schema, Key>[];
|
|
248
|
+
/** The ordering column that makes the ordering total. Defaults to `id`. */
|
|
249
|
+
uniqueBy?: keyof PlainRow<Schema, Key> & string;
|
|
250
|
+
/** Query parameter name a caller should send the cursor under. Defaults to `cursor`. */
|
|
251
|
+
cursorName?: string;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* One model's cursor reader.
|
|
255
|
+
*
|
|
256
|
+
* The argument goes through ZenStack's own `SelectSubset`, which is what a
|
|
257
|
+
* model's `findMany` uses and is the whole of the refusal machinery here. It
|
|
258
|
+
* maps any key the arguments do not declare to `never` — restoring the
|
|
259
|
+
* excess-property refusal a bare type parameter switches off, so `skip`,
|
|
260
|
+
* `distinct` and the native `cursor` are rejected — and it carries ZenStack's
|
|
261
|
+
* conditional messages for `select` with `include` and `select` with `omit`.
|
|
262
|
+
* Restating those exclusions by hand would answer the same mistake with a worse
|
|
263
|
+
* message; rebuilding the argument type without it drops them silently, which
|
|
264
|
+
* is what an earlier revision of this file did.
|
|
265
|
+
*
|
|
266
|
+
* `Args` is still inferred from the call, so `select` narrows the row type.
|
|
267
|
+
*/
|
|
268
|
+
interface CursorModelReader<Schema extends SchemaDef$1, Key extends CursorModelKey<Schema>> {
|
|
269
|
+
findMany<Args extends CursorReaderArgs<Schema, Key>>(args: SelectSubset<Args, CursorReaderArgs<Schema, Key>>): Promise<CursorPageResult<SimplifiedPlainResult<Schema, ModelOf<Schema, Key>, Args, Options<Schema>>>>;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Arguments for a page read from a caller-supplied `findMany`.
|
|
273
|
+
*
|
|
274
|
+
* The row type comes from the delegate — the framework has no schema for a
|
|
275
|
+
* `UNION` — and the ordering is keyed to it, so ordering on a column the query
|
|
276
|
+
* does not return is a compile error instead of the `CursorOrderingError` it
|
|
277
|
+
* would raise once the first row came back.
|
|
278
|
+
*
|
|
279
|
+
* No `select`, `include` or `omit`. They are ZenStack model arguments; a
|
|
280
|
+
* caller-written `findMany` decides for itself what to return, and the framework
|
|
281
|
+
* can neither type them against the row nor make the delegate honour them — an
|
|
282
|
+
* option that may be silently ignored is worse than one that is absent. A
|
|
283
|
+
* caller that wants fewer columns selects fewer inside its own `findMany`.
|
|
284
|
+
*
|
|
285
|
+
* `where` stays, and is not a passthrough: the paginator combines it with the
|
|
286
|
+
* keyset condition it builds, so it is how the page is positioned at all. It
|
|
287
|
+
* cannot be typed here because its shape is the delegate's own filter language.
|
|
288
|
+
*/
|
|
289
|
+
interface CursorSourceArgs<TRow> {
|
|
290
|
+
/**
|
|
291
|
+
* The cursor to read from, or `null`/absent to start at the beginning of the
|
|
292
|
+
* ordering. Opaque: it is minted by a previous result and passed back
|
|
293
|
+
* verbatim. Never construct one.
|
|
294
|
+
*/
|
|
295
|
+
cursor?: string | null;
|
|
296
|
+
/** Rows per page. The last page may hold fewer. */
|
|
297
|
+
take: number;
|
|
298
|
+
/**
|
|
299
|
+
* Ordering, applied left to right. Required, and must end in a unique column
|
|
300
|
+
* — a cursor addresses a row's position in an ordering, and a position inside
|
|
301
|
+
* a tie cannot be addressed.
|
|
302
|
+
*/
|
|
303
|
+
orderBy: CursorSourceOrderBy<TRow> | CursorSourceOrderBy<TRow>[];
|
|
304
|
+
/** The ordering column that makes the ordering total. Defaults to `id`. */
|
|
305
|
+
uniqueBy?: CursorSourceColumn<TRow>;
|
|
306
|
+
/** Filter, combined with the cursor's own condition and handed to the delegate. */
|
|
307
|
+
where?: Record<string, unknown>;
|
|
308
|
+
/** Query parameter name a caller should send the cursor under. Defaults to `cursor`. */
|
|
309
|
+
cursorName?: string;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* A column of the delegate's row.
|
|
313
|
+
*
|
|
314
|
+
* `DeclaredMembers` strips the index signature that {@link CursorPageDelegate}'s
|
|
315
|
+
* row constraint obliges a caller to write (`interface Row extends
|
|
316
|
+
* Record<string, unknown>`). Without it `keyof` is `string` and every name
|
|
317
|
+
* typechecks, which is the whole of the guarantee here.
|
|
318
|
+
*/
|
|
319
|
+
type CursorSourceColumn<TRow> = keyof DeclaredMembers<TRow> & string;
|
|
320
|
+
/** An ordering clause over the delegate's own columns. */
|
|
321
|
+
type CursorSourceOrderBy<TRow> = Partial<Record<CursorSourceColumn<TRow>, CursorSortOrder>>;
|
|
322
|
+
/**
|
|
323
|
+
* Reads a page from something that is not a model — a `UNION`, a raw statement —
|
|
324
|
+
* by supplying the `findMany` yourself.
|
|
325
|
+
*
|
|
326
|
+
* Sits beside the model keys rather than in a separate export because it is the
|
|
327
|
+
* same operation on a different source, and because a free function has no
|
|
328
|
+
* client to belong to: on a transaction client, `tx.$cursor.$from(…)` reads
|
|
329
|
+
* inside the transaction, which an imported function could not do.
|
|
330
|
+
*
|
|
331
|
+
* The `$` prefix is what keeps it from colliding with a model: a client key is
|
|
332
|
+
* an uncapitalized model name, and ZenStack reserves the prefix for members of
|
|
333
|
+
* the client itself.
|
|
334
|
+
*/
|
|
335
|
+
interface CursorSourceReader {
|
|
336
|
+
$from<TRow extends Record<string, unknown>>(delegate: CursorPageDelegate<TRow>, args: CursorSourceArgs<NoInfer<TRow>>): Promise<CursorPageResult<TRow>>;
|
|
337
|
+
}
|
|
338
|
+
/** `db.$cursor` — one reader per model in the schema, plus `$from` for anything else. */
|
|
339
|
+
type CursorReader<Schema extends SchemaDef$1> = { [Key in CursorModelKey<Schema>]: CursorModelReader<Schema, Key>; } & CursorSourceReader;
|
|
340
|
+
/** The client members {@link createCursorReader} contributes. */
|
|
341
|
+
interface CursorClientMembers<Schema extends SchemaDef$1> {
|
|
342
|
+
/**
|
|
343
|
+
* Reads one page of a model's rows by cursor, for a list that changes while
|
|
344
|
+
* it is read.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* const page = await db.$cursor.thread.findMany({
|
|
349
|
+
* cursor: ctx.query('cursor'),
|
|
350
|
+
* take: 20,
|
|
351
|
+
* orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],
|
|
352
|
+
* where: { boardId },
|
|
353
|
+
* })
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
$cursor: CursorReader<Schema>;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Builds the reader eagerly, one entry per model the schema declares.
|
|
360
|
+
*
|
|
361
|
+
* A plain object rather than a `Proxy`: the model list is known here, and a
|
|
362
|
+
* proxy would answer every property — `then`, `constructor`, an inspector's
|
|
363
|
+
* probe — with something that looks like a reader.
|
|
364
|
+
*
|
|
365
|
+
* The delegate is resolved per call, not captured: ZenStack builds a fresh CRUD
|
|
366
|
+
* handler on each model access, and holding one would pin it for the life of
|
|
367
|
+
* the client.
|
|
368
|
+
*/
|
|
369
|
+
declare function createCursorReader<Schema extends SchemaDef$1>(client: unknown, schema: Schema): CursorReader<Schema>;
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/database/database.service.d.ts
|
|
372
|
+
/**
|
|
373
|
+
* Client members every connection carries, because `createDatabaseService`
|
|
374
|
+
* defines them on every client it builds — the base client and the transaction
|
|
375
|
+
* client alike.
|
|
376
|
+
*
|
|
377
|
+
* Intersected onto the client from outside rather than passed as its
|
|
378
|
+
* `ExtClientMembers` argument: that slot is constrained to
|
|
379
|
+
* `Record<string, unknown>`, and the index signature it forces survives
|
|
380
|
+
* `$transaction`'s `Omit` and collapses every model delegate on a transaction
|
|
381
|
+
* client to `unknown`.
|
|
382
|
+
*/
|
|
383
|
+
type BuiltInClientMembers<K extends ConnectionName> = CursorClientMembers<InferConnectionSchema<K>>;
|
|
384
|
+
/** The ZenStack client for a connection, before the built-in members are added. */
|
|
385
|
+
type ConnectionClient<K extends ConnectionName> = ClientContract<InferConnectionSchema<K>, ClientOptions<InferConnectionSchema<K>>, InferConnectionExtensions<K>['extQueryArgs'], InferConnectionExtensions<K>['extClientMembers'], InferConnectionExtensions<K>['extResult']>;
|
|
386
|
+
/** The client a `$transaction` callback receives, carrying the same built-in members. */
|
|
387
|
+
type TransactionService<K extends ConnectionName = DefaultConnectionName> = TransactionClientContract<InferConnectionSchema<K>, ClientOptions<InferConnectionSchema<K>>, InferConnectionExtensions<K>['extQueryArgs'], InferConnectionExtensions<K>['extClientMembers'], InferConnectionExtensions<K>['extResult']> & BuiltInClientMembers<K>;
|
|
388
|
+
/** `{ [I]: Awaited<P[I]> }`, matching what the sequential form of `$transaction` resolves to. */
|
|
389
|
+
type UnwrapPromises<P extends readonly unknown[]> = { [I in keyof P]: Awaited<P[I]>; };
|
|
390
|
+
/**
|
|
391
|
+
* `$transaction`, restated so the interactive form hands back a client carrying
|
|
392
|
+
* the built-in members. ZenStack types its callback with its own
|
|
393
|
+
* `TransactionClientContract`, which knows nothing of them.
|
|
394
|
+
*
|
|
395
|
+
* Intersected ahead of the client rather than replacing its `$transaction`,
|
|
396
|
+
* because overload resolution across an intersection tries the constituents in
|
|
397
|
+
* order and these have to be reached first. `Omit`ing the original is not the
|
|
398
|
+
* alternative it looks like: with an unresolved `SchemaDef` the client's model
|
|
399
|
+
* delegates are an index signature, so `keyof` is `string`, `Omit` collapses
|
|
400
|
+
* every named member into it, and `$queryRawUnsafe` starts resolving to a model
|
|
401
|
+
* delegate.
|
|
402
|
+
*
|
|
403
|
+
* The sequential form is restated unchanged — it is only here because an
|
|
404
|
+
* overload set has to be declared together.
|
|
405
|
+
*/
|
|
406
|
+
interface TransactionWithBuiltIns<K extends ConnectionName> {
|
|
407
|
+
$transaction<T>(callback: (tx: TransactionService<K>) => Promise<T>, options?: {
|
|
408
|
+
isolationLevel?: TransactionIsolationLevel;
|
|
409
|
+
}): Promise<T>;
|
|
410
|
+
$transaction<P extends readonly PromiseLike<unknown>[]>(arg: [...P], options?: {
|
|
411
|
+
isolationLevel?: TransactionIsolationLevel;
|
|
412
|
+
}): Promise<UnwrapPromises<P>>;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* DatabaseService type
|
|
416
|
+
*
|
|
417
|
+
* Each connection has its own schema and plugin extensions.
|
|
418
|
+
* Plugin extension types are automatically inferred from `StratalDatabase.plugins`.
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```typescript
|
|
422
|
+
* // Typed to default connection (includes plugin extensions)
|
|
423
|
+
* constructor(@inject(DI_TOKENS.Database) private db: DatabaseService) {}
|
|
424
|
+
*
|
|
425
|
+
* // Typed to a specific named connection
|
|
426
|
+
* constructor(@InjectDB('analytics') private analytics: DatabaseService<'analytics'>) {}
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
type DatabaseService<K extends ConnectionName = DefaultConnectionName> = TransactionWithBuiltIns<K> & ConnectionClient<K> & BuiltInClientMembers<K>;
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/database/database.tokens.d.ts
|
|
432
|
+
declare const DATABASE_TOKENS: {
|
|
433
|
+
readonly Options: symbol;
|
|
434
|
+
readonly Services: symbol;
|
|
435
|
+
};
|
|
436
|
+
declare function connectionSymbol(name: ConnectionName): symbol;
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/database/pool.d.ts
|
|
439
|
+
/**
|
|
440
|
+
* Env binding the framework reads to decide DB connection topology. Set by
|
|
441
|
+
* `@stratal/testing` (the test harness runs against a DIRECT Postgres with no
|
|
442
|
+
* Hyperdrive in front); never set in dev / staging / production.
|
|
443
|
+
*/
|
|
444
|
+
declare const DB_SHARED_POOL_ENV = "STRATAL_DB_SHARED_POOL";
|
|
445
|
+
/**
|
|
446
|
+
* Build the lazy pool factory a consumer hands to its ZenStack dialect,
|
|
447
|
+
* choosing the connection topology from the environment instead of hard-coding
|
|
448
|
+
* one. The consumer writes `dialect: () => new PostgresDialect({ pool })` where
|
|
449
|
+
* `pool = createPoolFactory(env, () => new Pool(poolConfig))` — so the prod-vs-
|
|
450
|
+
* test decision lives here, in the framework, not as an `IS_TEST` branch in app
|
|
451
|
+
* config.
|
|
452
|
+
*
|
|
453
|
+
* - **Default (dev / staging / production):** returns a FRESH pool on every
|
|
454
|
+
* call. The dialect is rebuilt per request resolution, so each request owns
|
|
455
|
+
* its own pool/socket — mandatory on workerd, where a pool opened in one
|
|
456
|
+
* request's I/O context cannot be reused by another (the cross-request I/O is
|
|
457
|
+
* cancelled and the request hangs forever). Hyperdrive fronts these pools and
|
|
458
|
+
* multiplexes the real server connections, so they never accumulate.
|
|
459
|
+
* - **Shared (`DB_SHARED_POOL_ENV === 'true'`):** memoizes ONE pool per
|
|
460
|
+
* connection, reused across every resolution. `@stratal/testing` sets this
|
|
461
|
+
* because the test harness hits a direct Postgres with no Hyperdrive — a fresh
|
|
462
|
+
* pool per resolution would exhaust `max_connections` across parallel test
|
|
463
|
+
* files. One shared pool per connection mirrors what Hyperdrive does in prod.
|
|
464
|
+
*
|
|
465
|
+
* This single pool is forced **persistent** ({@link withPersistentConnection}):
|
|
466
|
+
* its idle reaper is disabled regardless of the config `makePool` passed. A pool
|
|
467
|
+
* reused for the whole worker run must never idle-close — a non-zero
|
|
468
|
+
* `idleTimeoutMillis` reaps its connection between operations, and under real
|
|
469
|
+
* network latency (a CI Postgres service, not a local socket) that eviction
|
|
470
|
+
* races in-flight and subsequent queries → "Connection terminated unexpectedly",
|
|
471
|
+
* cascading into half-applied writes and cross-test row leakage. It passes on a
|
|
472
|
+
* fast local socket and only surfaces under latency, so consumers can't be
|
|
473
|
+
* trusted to configure it right — the shared branch enforces it. The consumer's
|
|
474
|
+
* own idle settings still apply to the fresh-per-resolution prod pools (below).
|
|
475
|
+
*
|
|
476
|
+
* Either way the pool is created LAZILY: `makePool` is invoked by Kysely on the
|
|
477
|
+
* first query, inside the request's I/O context — never at module-eval / global
|
|
478
|
+
* scope, which workerd forbids ("Disallowed operation within global scope").
|
|
479
|
+
*/
|
|
480
|
+
declare function createPoolFactory<TPool>(env: object, makePool: () => TPool): () => Promise<TPool>;
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/database/decorators/inject-db.decorator.d.ts
|
|
483
|
+
declare function InjectDB(name: ConnectionName): ParameterDecorator;
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/database/errors/cursor-model-unavailable.error.d.ts
|
|
486
|
+
/**
|
|
487
|
+
* Raised when `db.$cursor.<model>` cannot reach a model delegate on the client
|
|
488
|
+
* it was built for — the schema declares the model, the client does not answer
|
|
489
|
+
* for it.
|
|
490
|
+
*
|
|
491
|
+
* This is a **programming error**, not bad input: the reader is built from the
|
|
492
|
+
* schema's own model list, so reaching it means the client and the schema it was
|
|
493
|
+
* given have come apart — a model sliced out of the client's options, or a
|
|
494
|
+
* hand-assembled client. No request recovers from it and no retry helps. It
|
|
495
|
+
* deliberately carries no HTTP status, so it is never mistaken for something the
|
|
496
|
+
* client sent wrong.
|
|
497
|
+
*
|
|
498
|
+
* `model` names the client key at fault, and is reported to observability so the
|
|
499
|
+
* raise site stays distinguishable without matching on message text.
|
|
500
|
+
*/
|
|
501
|
+
declare class CursorModelUnavailableError extends ApplicationError {
|
|
502
|
+
readonly model: string;
|
|
503
|
+
constructor(model: string);
|
|
504
|
+
reportContext(): Record<string, unknown> | undefined;
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/database/errors/cursor-ordering.error.d.ts
|
|
508
|
+
/**
|
|
509
|
+
* Raised when a `$cursor` read is asked for an ordering it cannot address a row
|
|
510
|
+
* in: no `orderBy`, a clause with no `asc`/`desc` direction, an ordering
|
|
511
|
+
* without the unique column that breaks ties, or an ordering column that is
|
|
512
|
+
* absent from the returned row or null on it.
|
|
513
|
+
*
|
|
514
|
+
* This is a **programming error**, not bad input — no request can recover from
|
|
515
|
+
* it and no retry helps, because the query as written cannot produce a stable
|
|
516
|
+
* position. Handlers should let it surface as a `500` and report it; the fix is
|
|
517
|
+
* always in the caller's `orderBy`, `uniqueBy` or `select`. It deliberately
|
|
518
|
+
* carries no HTTP status, so it is never mistaken for something the client sent
|
|
519
|
+
* wrong.
|
|
520
|
+
*
|
|
521
|
+
* `field` names the ordering column at fault where one is identifiable, and is
|
|
522
|
+
* reported to observability so the raise sites stay distinguishable without
|
|
523
|
+
* matching on message text.
|
|
524
|
+
*/
|
|
525
|
+
declare class CursorOrderingError extends ApplicationError {
|
|
526
|
+
readonly field?: string | undefined;
|
|
527
|
+
constructor(message: string, field?: string | undefined);
|
|
528
|
+
reportContext(): Record<string, unknown> | undefined;
|
|
529
|
+
}
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/database/errors/malformed-cursor.error.d.ts
|
|
532
|
+
/**
|
|
533
|
+
* Raised when a pagination cursor cannot be read: it is not the base64url
|
|
534
|
+
* payload `$cursor` mints, or it decodes to something that is not a cursor.
|
|
535
|
+
*
|
|
536
|
+
* This is **bad input**, not a broken query. A cursor travels in a query string,
|
|
537
|
+
* so a truncated link, a hand-edited URL, or one minted by an older build all
|
|
538
|
+
* land here, and the request itself is still answerable. It carries its own
|
|
539
|
+
* `400`, so a handler lets it surface rather than catching it to serve the
|
|
540
|
+
* first page — the same refusal any other damaged parameter gets.
|
|
541
|
+
*/
|
|
542
|
+
declare class MalformedCursorError extends HttpException {
|
|
543
|
+
constructor(cause?: unknown);
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/database/errors/record-not-found.error.d.ts
|
|
547
|
+
declare class RecordNotFoundError extends HttpException {
|
|
548
|
+
readonly details?: string | undefined;
|
|
549
|
+
constructor(details?: string | undefined, cause?: unknown);
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/database/errors/unique-constraint.error.d.ts
|
|
553
|
+
declare class UniqueConstraintError extends HttpException {
|
|
554
|
+
readonly fields?: string[] | undefined;
|
|
555
|
+
constructor(fields?: string[] | undefined, cause?: unknown);
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/database/errors/from-zenstack-error.d.ts
|
|
559
|
+
declare function fromZenStackError(error: unknown): ApplicationError;
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/database/event-types.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* Event phase: before or after operation
|
|
564
|
+
*/
|
|
565
|
+
type EventPhase = 'before' | 'after';
|
|
566
|
+
/**
|
|
567
|
+
* All database operations that can trigger events
|
|
568
|
+
*/
|
|
569
|
+
type DatabaseOperation = AllCrudOperations;
|
|
570
|
+
/**
|
|
571
|
+
* Distributive helper — extracts model names from each schema member individually.
|
|
572
|
+
* Using a naked type parameter ensures TypeScript distributes over unions:
|
|
573
|
+
* `_ExtractModelNames<A | B>` = `_ExtractModelNames<A> | _ExtractModelNames<B>`
|
|
574
|
+
*/
|
|
575
|
+
type _ExtractModelNames<S> = S extends {
|
|
576
|
+
models: infer M;
|
|
577
|
+
} ? Extract<keyof M, string> : never;
|
|
578
|
+
/**
|
|
579
|
+
* Model names derived from the shared database schema.
|
|
580
|
+
* Distributes over all schema types so models from every connection are included.
|
|
581
|
+
*/
|
|
582
|
+
type ModelName = _ExtractModelNames<InferAnySchema>;
|
|
583
|
+
/**
|
|
584
|
+
* Database event names with all supported patterns.
|
|
585
|
+
*/
|
|
586
|
+
type DatabaseEventName = `${EventPhase}.${ModelName}.${DatabaseOperation}` | `${EventPhase}.${ModelName}` | `${EventPhase}.${DatabaseOperation}` | EventPhase;
|
|
587
|
+
/**
|
|
588
|
+
* Map operation name to ZenStack Args type for a given schema and model
|
|
589
|
+
*/
|
|
590
|
+
type OperationArgsMap<S extends SchemaDef, M extends Extract<keyof S['models'], string>, O extends DatabaseOperation> = O extends 'create' ? CreateArgs<S, M> : O extends 'createMany' ? CreateManyArgs<S, M> : O extends 'update' ? UpdateArgs<S, M> : O extends 'updateMany' ? UpdateManyArgs<S, M> : O extends 'delete' ? DeleteArgs<S, M> : O extends 'deleteMany' ? DeleteManyArgs<S, M> : O extends 'findUnique' ? FindUniqueArgs<S, M> : O extends 'findFirst' ? FindFirstArgs<S, M> : O extends 'findMany' ? FindManyArgs<S, M> : O extends 'upsert' ? UpsertArgs<S, M> : O extends 'count' ? CountArgs<S, M> : O extends 'aggregate' ? AggregateArgs<S, M> : O extends 'groupBy' ? GroupByArgs<S, M> : never;
|
|
591
|
+
/**
|
|
592
|
+
* Distributive helper — resolves data/where args for a model against each schema individually.
|
|
593
|
+
*/
|
|
594
|
+
type _ExtractData<S, M extends string, O extends DatabaseOperation> = S extends SchemaDef ? M extends Extract<keyof S['models'], string> ? OperationArgsMap<S, M, O> extends {
|
|
595
|
+
data: infer D;
|
|
596
|
+
} ? D : OperationArgsMap<S, M, O> extends {
|
|
597
|
+
where: infer W;
|
|
598
|
+
} ? W : OperationArgsMap<S, M, O> : never : never;
|
|
599
|
+
/**
|
|
600
|
+
* Extract the data/where property from operation args.
|
|
601
|
+
* Distributes over all schemas to find the matching model.
|
|
602
|
+
*/
|
|
603
|
+
type GetData<M extends ModelName, O extends DatabaseOperation> = _ExtractData<InferAnySchema, M, O> extends never ? unknown : _ExtractData<InferAnySchema, M, O>;
|
|
604
|
+
/**
|
|
605
|
+
* Distributive helper — resolves result type for a model against each schema individually.
|
|
606
|
+
*/
|
|
607
|
+
type _ExtractResult<S, M extends string, O extends DatabaseOperation> = S extends SchemaDef ? M extends Extract<keyof S['models'], string> ? O extends 'findMany' | 'createMany' | 'updateMany' | 'deleteMany' ? ModelResult<S, M>[] : O extends 'count' ? number : ModelResult<S, M> : never : never;
|
|
608
|
+
/**
|
|
609
|
+
* Extract result type for a model operation.
|
|
610
|
+
* Distributes over all schemas to find the matching model.
|
|
611
|
+
*/
|
|
612
|
+
type GetResult<M extends ModelName, O extends DatabaseOperation> = _ExtractResult<InferAnySchema, M, O> extends never ? unknown : _ExtractResult<InferAnySchema, M, O>;
|
|
613
|
+
/**
|
|
614
|
+
* Verb suffix for entity-mutation events (`entity.{Model}.{verb}`).
|
|
615
|
+
*/
|
|
616
|
+
type EntityMutationVerb = 'created' | 'updated' | 'deleted';
|
|
617
|
+
/**
|
|
618
|
+
* Entity-mutation event names with all supported patterns.
|
|
619
|
+
*
|
|
620
|
+
* Unlike `before.*`/`after.*` (raw query args/result), entity events carry
|
|
621
|
+
* full entity snapshots: `created` has `after`, `updated` has `before` and
|
|
622
|
+
* `after`, `deleted` has `before`. The pre-mutation snapshot is loaded inside
|
|
623
|
+
* the mutation's transaction, and only when a listener matches — a wildcard
|
|
624
|
+
* subscription (`entity`) makes every model pay that pre-read, so subscribe
|
|
625
|
+
* per model when cost matters.
|
|
626
|
+
*/
|
|
627
|
+
type EntityEventName = `entity.${ModelName}.${EntityMutationVerb}` | `entity.${ModelName}` | `entity.${EntityMutationVerb}` | 'entity';
|
|
628
|
+
/**
|
|
629
|
+
* Distributive helper — resolves the full entity shape for a model.
|
|
630
|
+
*/
|
|
631
|
+
type _ExtractEntity<S, M extends string> = S extends SchemaDef ? M extends Extract<keyof S['models'], string> ? ModelResult<S, M> : never : never;
|
|
632
|
+
/**
|
|
633
|
+
* Full entity snapshot type for a model, across all connections' schemas.
|
|
634
|
+
*/
|
|
635
|
+
type GetEntity<M extends ModelName> = _ExtractEntity<InferAnySchema, M> extends never ? unknown : _ExtractEntity<InferAnySchema, M>;
|
|
636
|
+
interface EntityCreatedEventContext<M extends ModelName> {
|
|
637
|
+
model: M;
|
|
638
|
+
action: 'created';
|
|
639
|
+
before: undefined;
|
|
640
|
+
after: GetEntity<M>;
|
|
641
|
+
}
|
|
642
|
+
interface EntityUpdatedEventContext<M extends ModelName> {
|
|
643
|
+
model: M;
|
|
644
|
+
action: 'updated';
|
|
645
|
+
before: GetEntity<M>;
|
|
646
|
+
after: GetEntity<M>;
|
|
647
|
+
}
|
|
648
|
+
interface EntityDeletedEventContext<M extends ModelName> {
|
|
649
|
+
model: M;
|
|
650
|
+
action: 'deleted';
|
|
651
|
+
before: GetEntity<M>;
|
|
652
|
+
after: undefined;
|
|
653
|
+
}
|
|
654
|
+
/** Context for model wildcard subscriptions (e.g. "entity.User") */
|
|
655
|
+
interface EntityModelWildcardContext<M extends ModelName> {
|
|
656
|
+
model: M;
|
|
657
|
+
action: EntityMutationVerb;
|
|
658
|
+
before: GetEntity<M> | undefined;
|
|
659
|
+
after: GetEntity<M> | undefined;
|
|
660
|
+
}
|
|
661
|
+
/** Context for verb wildcard subscriptions (e.g. "entity.updated") */
|
|
662
|
+
interface EntityVerbWildcardContext<V extends EntityMutationVerb> {
|
|
663
|
+
model: ModelName;
|
|
664
|
+
action: V;
|
|
665
|
+
before: V extends 'created' ? undefined : unknown;
|
|
666
|
+
after: V extends 'deleted' ? undefined : unknown;
|
|
667
|
+
}
|
|
668
|
+
/** Context for the global wildcard subscription ("entity") */
|
|
669
|
+
interface EntityWildcardContext {
|
|
670
|
+
model: ModelName;
|
|
671
|
+
action: EntityMutationVerb;
|
|
672
|
+
before: unknown;
|
|
673
|
+
after: unknown;
|
|
674
|
+
}
|
|
675
|
+
type EntityEventContext<E extends string> = E extends `entity.${infer M extends ModelName}.created` ? EntityCreatedEventContext<M> : E extends `entity.${infer M extends ModelName}.updated` ? EntityUpdatedEventContext<M> : E extends `entity.${infer M extends ModelName}.deleted` ? EntityDeletedEventContext<M> : E extends `entity.${infer M extends ModelName}` ? EntityModelWildcardContext<M> : E extends `entity.${infer V extends EntityMutationVerb}` ? EntityVerbWildcardContext<V> : EntityWildcardContext;
|
|
676
|
+
/**
|
|
677
|
+
* Mapped type producing all entity-mutation event name → context pairs.
|
|
678
|
+
*/
|
|
679
|
+
type EntityEvents = { [E in EntityEventName]: EntityEventContext<E>; };
|
|
680
|
+
/**
|
|
681
|
+
* Parse event string into structured type for discriminated unions
|
|
682
|
+
*/
|
|
683
|
+
type ParseEvent<E extends string> = E extends `${infer Phase extends EventPhase}.${infer Model extends ModelName}.${infer Op extends DatabaseOperation}` ? {
|
|
684
|
+
phase: Phase;
|
|
685
|
+
model: Model;
|
|
686
|
+
operation: Op;
|
|
687
|
+
type: 'exact';
|
|
688
|
+
} : E extends `${infer Phase extends EventPhase}.${infer Second}` ? Second extends ModelName ? {
|
|
689
|
+
phase: Phase;
|
|
690
|
+
model: Second;
|
|
691
|
+
type: 'model-wildcard';
|
|
692
|
+
} : Second extends DatabaseOperation ? {
|
|
693
|
+
phase: Phase;
|
|
694
|
+
operation: Second;
|
|
695
|
+
type: 'operation-wildcard';
|
|
696
|
+
} : never : E extends EventPhase ? {
|
|
697
|
+
phase: E;
|
|
698
|
+
type: 'phase-wildcard';
|
|
699
|
+
} : never;
|
|
700
|
+
/** Base context fields present in all events */
|
|
701
|
+
interface BaseEventContext {}
|
|
702
|
+
/** Context for exact database events (e.g., "after.User.create") */
|
|
703
|
+
interface ExactDatabaseEventContext<M extends ModelName, O extends DatabaseOperation, Phase extends EventPhase> extends BaseEventContext {
|
|
704
|
+
data: Phase extends 'before' ? GetData<M, O> : Readonly<GetData<M, O>>;
|
|
705
|
+
result: Phase extends 'after' ? GetResult<M, O> : undefined;
|
|
706
|
+
}
|
|
707
|
+
/** Context for model wildcard events (e.g., "after.User") */
|
|
708
|
+
interface ModelWildcardEventContext<Phase extends EventPhase> extends BaseEventContext {
|
|
709
|
+
operation: DatabaseOperation;
|
|
710
|
+
data: Phase extends 'before' ? unknown : Readonly<unknown>;
|
|
711
|
+
result: Phase extends 'after' ? unknown : undefined;
|
|
712
|
+
}
|
|
713
|
+
/** Context for operation wildcard events (e.g., "after.create") */
|
|
714
|
+
interface OperationWildcardEventContext<Phase extends EventPhase> extends BaseEventContext {
|
|
715
|
+
model: ModelName;
|
|
716
|
+
data: Phase extends 'before' ? unknown : Readonly<unknown>;
|
|
717
|
+
result: Phase extends 'after' ? unknown : undefined;
|
|
718
|
+
}
|
|
719
|
+
/** Context for phase wildcard events (e.g., "after" or "before") */
|
|
720
|
+
interface PhaseWildcardEventContext<Phase extends EventPhase> extends BaseEventContext {
|
|
721
|
+
model: ModelName;
|
|
722
|
+
operation: DatabaseOperation;
|
|
723
|
+
data: Phase extends 'before' ? unknown : Readonly<unknown>;
|
|
724
|
+
result: Phase extends 'after' ? unknown : undefined;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Type-safe event context with discriminated unions.
|
|
728
|
+
*/
|
|
729
|
+
type DatabaseEventContext<E extends string> = ParseEvent<E> extends {
|
|
730
|
+
phase: infer P extends EventPhase;
|
|
731
|
+
model: infer M extends ModelName;
|
|
732
|
+
operation: infer O extends DatabaseOperation;
|
|
733
|
+
type: 'exact';
|
|
734
|
+
} ? ExactDatabaseEventContext<M, O, P> : ParseEvent<E> extends {
|
|
735
|
+
phase: infer P extends EventPhase;
|
|
736
|
+
model: infer _M extends ModelName;
|
|
737
|
+
type: 'model-wildcard';
|
|
738
|
+
} ? ModelWildcardEventContext<P> : ParseEvent<E> extends {
|
|
739
|
+
phase: infer P extends EventPhase;
|
|
740
|
+
operation: infer _O extends DatabaseOperation;
|
|
741
|
+
type: 'operation-wildcard';
|
|
742
|
+
} ? OperationWildcardEventContext<P> : ParseEvent<E> extends {
|
|
743
|
+
phase: infer P extends EventPhase;
|
|
744
|
+
type: 'phase-wildcard';
|
|
745
|
+
} ? PhaseWildcardEventContext<P> : BaseEventContext;
|
|
746
|
+
/**
|
|
747
|
+
* Mapped type that produces all database event name to context pairs.
|
|
748
|
+
*
|
|
749
|
+
* Used to augment core's `CustomEventRegistry`:
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```typescript
|
|
753
|
+
* declare module 'stratal/events' {
|
|
754
|
+
* interface CustomEventRegistry extends DatabaseEvents {}
|
|
755
|
+
* }
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
type DatabaseEvents = { [E in DatabaseEventName]: DatabaseEventContext<E>; };
|
|
759
|
+
declare module 'stratal/events' {
|
|
760
|
+
interface CustomEventRegistry extends DatabaseEvents, EntityEvents {}
|
|
761
|
+
}
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region src/database/i18n/en.d.ts
|
|
764
|
+
declare const databaseMessages: {
|
|
765
|
+
readonly en: {
|
|
766
|
+
readonly connectionNameRequired: 'Connection name is required';
|
|
767
|
+
readonly defaultConnectionRequired: 'Default connection name is required';
|
|
768
|
+
readonly connectionRequired: 'At least one connection is required';
|
|
769
|
+
readonly duplicateConnections: 'Duplicate connection names found';
|
|
770
|
+
readonly defaultConnectionNotFound: 'Default connection not found in connections';
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
declare module 'stratal/i18n' {
|
|
774
|
+
interface AppMessageNamespaces {
|
|
775
|
+
database: typeof databaseMessages['en'];
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/database/plugins/error-handler.plugin.d.ts
|
|
780
|
+
/**
|
|
781
|
+
* ZenStack runtime plugin that transforms ORM errors into ApplicationError instances.
|
|
782
|
+
*
|
|
783
|
+
* @example
|
|
784
|
+
* ```typescript
|
|
785
|
+
* super(schema, {
|
|
786
|
+
* dialect: new PostgresDialect({ pool }),
|
|
787
|
+
* plugins: [new ErrorHandlerPlugin()]
|
|
788
|
+
* })
|
|
789
|
+
* ```
|
|
790
|
+
*/
|
|
791
|
+
declare class ErrorHandlerPlugin implements RuntimePlugin<SchemaDef$1, Record<string, unknown>, Record<string, unknown>, {}> {
|
|
792
|
+
readonly id = "error-handler";
|
|
793
|
+
onQuery: ({ args, proceed }: {
|
|
794
|
+
args: Record<string, unknown> | undefined;
|
|
795
|
+
proceed: (args: Record<string, unknown> | undefined) => Promise<unknown>;
|
|
796
|
+
}) => Promise<unknown>;
|
|
797
|
+
}
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/database/plugins/event-emitter.plugin.d.ts
|
|
800
|
+
interface EventEmitterPluginOptions {
|
|
801
|
+
eventRegistry: IEventRegistry;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* ZenStack runtime plugin that emits before/after events for database operations.
|
|
805
|
+
*
|
|
806
|
+
* Emits events in the format:
|
|
807
|
+
* - `before.{Model}.{operation}` - Before the database operation
|
|
808
|
+
* - `after.{Model}.{operation}` - After the database operation
|
|
809
|
+
*
|
|
810
|
+
* Additionally emits entity-mutation events carrying full entity snapshots:
|
|
811
|
+
* - `entity.{Model}.created` - `{ after }`
|
|
812
|
+
* - `entity.{Model}.updated` - `{ before, after }`
|
|
813
|
+
* - `entity.{Model}.deleted` - `{ before }`
|
|
814
|
+
*
|
|
815
|
+
* Entity events are listener-driven: the pre-mutation snapshot is only
|
|
816
|
+
* loaded (inside the mutation's transaction) when `hasListeners()` reports
|
|
817
|
+
* a matching subscription, so models nobody observes pay no cost. Note that
|
|
818
|
+
* a wildcard subscription (`entity`) therefore makes every model pay the
|
|
819
|
+
* pre-read — subscribe per model when cost matters.
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```typescript
|
|
823
|
+
* super(schema, {
|
|
824
|
+
* dialect: new PostgresDialect({ pool }),
|
|
825
|
+
* plugins: [
|
|
826
|
+
* new EventEmitterPlugin({
|
|
827
|
+
* eventRegistry,
|
|
828
|
+
* })
|
|
829
|
+
* ]
|
|
830
|
+
* })
|
|
831
|
+
* ```
|
|
832
|
+
*/
|
|
833
|
+
declare class EventEmitterPlugin implements RuntimePlugin<SchemaDef$1, Record<string, unknown>, Record<string, unknown>, {}> {
|
|
834
|
+
private options;
|
|
835
|
+
readonly id = "event-emitter";
|
|
836
|
+
constructor(options: EventEmitterPluginOptions);
|
|
837
|
+
onEntityMutation: EntityMutationHooksDef<SchemaDef$1>;
|
|
838
|
+
onQuery: ({ model, operation, args, proceed }: {
|
|
839
|
+
model: string;
|
|
840
|
+
operation: string;
|
|
841
|
+
args: Record<string, unknown> | undefined;
|
|
842
|
+
proceed: (args: Record<string, unknown> | undefined) => Promise<unknown>;
|
|
843
|
+
}) => Promise<unknown>;
|
|
844
|
+
}
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region src/database/plugins/schema-switcher.d.ts
|
|
847
|
+
/**
|
|
848
|
+
* Switches the active schema on a ZenStack/Kysely database client by mutating
|
|
849
|
+
* `$schema.provider.defaultSchema`. This causes ZenStack's QueryNameMapper to
|
|
850
|
+
* generate fully-qualified table references (e.g. `"tenant_123"."User"`).
|
|
851
|
+
*
|
|
852
|
+
* Must be called BEFORE any queries are made on the client.
|
|
853
|
+
*
|
|
854
|
+
* Note: The ZenStack RuntimePlugin `onQuery` hook fires after table names are
|
|
855
|
+
* already resolved, so a plugin-based approach cannot set the schema prefix.
|
|
856
|
+
* Direct client mutation is the only supported method.
|
|
857
|
+
*/
|
|
858
|
+
declare class SchemaSwitcher {
|
|
859
|
+
static apply<T>(client: T, schemaName: string): T;
|
|
860
|
+
}
|
|
861
|
+
//#endregion
|
|
862
|
+
//#region src/database/commands/zenstack.command.d.ts
|
|
863
|
+
/**
|
|
864
|
+
* Base command for ZenStack CLI wrappers.
|
|
865
|
+
* Uses execFileSync with array arguments to prevent shell injection.
|
|
866
|
+
*/
|
|
867
|
+
declare abstract class ZenStackCommand extends Command {
|
|
868
|
+
protected zenstack(args: string[]): Promise<number>;
|
|
869
|
+
}
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/database/commands/db-generate.command.d.ts
|
|
872
|
+
declare class DbGenerateCommand extends ZenStackCommand {
|
|
873
|
+
static command: string;
|
|
874
|
+
static description: string;
|
|
875
|
+
handle(): Promise<number>;
|
|
876
|
+
}
|
|
877
|
+
//#endregion
|
|
878
|
+
//#region src/database/commands/db-pull.command.d.ts
|
|
879
|
+
declare class DbPullCommand extends ZenStackCommand {
|
|
880
|
+
static command: string;
|
|
881
|
+
static description: string;
|
|
882
|
+
handle(): Promise<number>;
|
|
883
|
+
}
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/database/commands/db-push.command.d.ts
|
|
886
|
+
declare class DbPushCommand extends ZenStackCommand {
|
|
887
|
+
static command: string;
|
|
888
|
+
static description: string;
|
|
889
|
+
handle(): Promise<number>;
|
|
890
|
+
}
|
|
891
|
+
//#endregion
|
|
892
|
+
//#region src/database/commands/migrate-deploy.command.d.ts
|
|
893
|
+
declare class MigrateDeployCommand extends ZenStackCommand {
|
|
894
|
+
static command: string;
|
|
895
|
+
static description: string;
|
|
896
|
+
handle(): Promise<number>;
|
|
897
|
+
}
|
|
898
|
+
//#endregion
|
|
899
|
+
//#region src/database/commands/migrate-dev.command.d.ts
|
|
900
|
+
declare class MigrateDevCommand extends ZenStackCommand {
|
|
901
|
+
static command: string;
|
|
902
|
+
static description: string;
|
|
903
|
+
handle(): Promise<number>;
|
|
904
|
+
}
|
|
905
|
+
//#endregion
|
|
906
|
+
//#region src/database/commands/migrate-reset.command.d.ts
|
|
907
|
+
declare class MigrateResetCommand extends ZenStackCommand {
|
|
908
|
+
static command: string;
|
|
909
|
+
static description: string;
|
|
910
|
+
handle(): Promise<number>;
|
|
911
|
+
}
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/database/commands/migrate-status.command.d.ts
|
|
914
|
+
declare class MigrateStatusCommand extends ZenStackCommand {
|
|
915
|
+
static command: string;
|
|
916
|
+
static description: string;
|
|
917
|
+
handle(): Promise<number>;
|
|
918
|
+
}
|
|
919
|
+
//#endregion
|
|
920
|
+
export { CursorPageDelegate as $, CursorOrderingError as A, CursorModelKey as B, GetResult as C, UniqueConstraintError as D, fromZenStackError as E, DATABASE_TOKENS as F, CursorSourceArgs as G, CursorOrderByOf as H, connectionSymbol as I, CursorSourceReader as J, CursorSourceColumn as K, DatabaseService as L, InjectDB as M, DB_SHARED_POOL_ENV as N, RecordNotFoundError as O, createPoolFactory as P, CursorPageArgs as Q, TransactionService as R, GetEntity as S, ParseEvent as T, CursorReader as U, CursorModelReader as V, CursorReaderArgs as W, CursorFindManyArgs as X, createCursorReader as Y, CursorOrderBy as Z, EntityEventName as _, DbPushCommand as a, DatabaseModule as at, EventPhase as b, ZenStackCommand as c, EventEmitterPluginOptions as d, CursorPageResult as et, ErrorHandlerPlugin as f, DatabaseOperation as g, DatabaseEvents as h, MigrateDeployCommand as i, DatabaseConnectionConfig as it, CursorModelUnavailableError as j, MalformedCursorError as k, SchemaSwitcher as l, DatabaseEventName as m, MigrateResetCommand as n, decodeCursor as nt, DbPullCommand as o, DatabaseModuleConfig as ot, databaseMessages as p, CursorSourceOrderBy as q, MigrateDevCommand as r, encodeCursor as rt, DbGenerateCommand as s, MigrateStatusCommand as t, CursorSortOrder as tt, EventEmitterPlugin as u, EntityEvents as v, ModelName as w, GetData as x, EntityMutationVerb as y, CursorClientMembers as z };
|
|
921
|
+
//# sourceMappingURL=index-e_u1SRyd.d.mts.map
|