@murumets-ee/entity 0.10.0 → 0.12.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/dist/admin/index.d.mts +472 -136
- package/dist/admin/index.d.mts.map +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/index.mjs.map +1 -1
- package/dist/index.d.mts +129 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/query/index.d.mts +98 -13
- package/dist/query/index.d.mts.map +1 -1
- package/dist/query/index.mjs +1 -1
- package/dist/query/index.mjs.map +1 -1
- package/dist/refs/index.d.mts +17 -0
- package/dist/refs/index.d.mts.map +1 -1
- package/dist/refs/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/admin/index.d.mts
CHANGED
|
@@ -36,78 +36,6 @@ interface CountCacheLike {
|
|
|
36
36
|
invalidate(prefix: string): void;
|
|
37
37
|
}
|
|
38
38
|
//#endregion
|
|
39
|
-
//#region src/cursor.d.ts
|
|
40
|
-
/** Cursor input for keyset pagination. */
|
|
41
|
-
interface CursorInput {
|
|
42
|
-
/** Sort field name (e.g. 'createdAt'). Must be a real column on the entity. */
|
|
43
|
-
field: string;
|
|
44
|
-
/** Last seen value of the sort field. */
|
|
45
|
-
value: string | number;
|
|
46
|
-
/** Sort direction — must match the ORDER BY direction. */
|
|
47
|
-
direction: 'asc' | 'desc';
|
|
48
|
-
/** Tie-breaker: last seen entity ID. Required for non-unique sort fields. */
|
|
49
|
-
id?: string;
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/admin-config.d.ts
|
|
53
|
-
/**
|
|
54
|
-
* Optional admin UI configuration for entities.
|
|
55
|
-
* Controls how entities appear in the admin sidebar, list pages, and forms.
|
|
56
|
-
*/
|
|
57
|
-
interface EntityAdminConfig {
|
|
58
|
-
/** Sidebar section: 'content' | 'structure' | custom string. Default: 'content' */
|
|
59
|
-
group?: string;
|
|
60
|
-
/** Plural display name for sidebar + list pages. Default: title-cased pluralized entity name */
|
|
61
|
-
label?: string;
|
|
62
|
-
/** Singular label for "New X" button. Default: title-cased entity name */
|
|
63
|
-
labelSingular?: string;
|
|
64
|
-
/** Lucide icon name as string, e.g. 'file-text' */
|
|
65
|
-
icon?: string;
|
|
66
|
-
/** Description shown on list page */
|
|
67
|
-
description?: string;
|
|
68
|
-
/** Form layout. Default: 'single' */
|
|
69
|
-
layout?: 'single' | 'two-column';
|
|
70
|
-
/** Fields to hide in the form */
|
|
71
|
-
hiddenFields?: string[];
|
|
72
|
-
/** Columns to hide in the list */
|
|
73
|
-
hiddenColumns?: string[];
|
|
74
|
-
/** Per-field label/description/placeholder overrides */
|
|
75
|
-
fieldOverrides?: Record<string, {
|
|
76
|
-
label?: string;
|
|
77
|
-
description?: string;
|
|
78
|
-
placeholder?: string;
|
|
79
|
-
}>;
|
|
80
|
-
/** Default list sort field. Default: 'createdAt' */
|
|
81
|
-
defaultSort?: string;
|
|
82
|
-
/** Default list sort direction. Default: 'desc' */
|
|
83
|
-
defaultSortDirection?: 'asc' | 'desc';
|
|
84
|
-
/** List page size. Default: 20 */
|
|
85
|
-
pageSize?: number;
|
|
86
|
-
/** For block editor: fields to show in Puck root config */
|
|
87
|
-
rootFields?: string[];
|
|
88
|
-
/** Suppress "New" button in the list page */
|
|
89
|
-
disableCreate?: boolean;
|
|
90
|
-
/** Order within sidebar group. Default: 0 */
|
|
91
|
-
sortOrder?: number;
|
|
92
|
-
/**
|
|
93
|
-
* Hide the entity from BOTH dashboard and sidebar.
|
|
94
|
-
* Use for system entities that should not appear in any UI.
|
|
95
|
-
* Shorthand for `hideFromMenu: true` + `hideFromDashboard: true`.
|
|
96
|
-
*/
|
|
97
|
-
hidden?: boolean;
|
|
98
|
-
/**
|
|
99
|
-
* Hide from sidebar menu only. Entity remains accessible via dashboard, direct URL,
|
|
100
|
-
* and the entity API. Use for storage models accessed through specialized UI
|
|
101
|
-
* (e.g., Ticket entities accessed via the inbox/board, not as a CRUD table).
|
|
102
|
-
*/
|
|
103
|
-
hideFromMenu?: boolean;
|
|
104
|
-
/**
|
|
105
|
-
* Hide from dashboard widget grid only. Entity still appears in sidebar.
|
|
106
|
-
* Use for entities that don't have meaningful counts/stats to show.
|
|
107
|
-
*/
|
|
108
|
-
hideFromDashboard?: boolean;
|
|
109
|
-
}
|
|
110
|
-
//#endregion
|
|
111
39
|
//#region src/fields/base.d.ts
|
|
112
40
|
/**
|
|
113
41
|
* Field type definitions
|
|
@@ -119,6 +47,23 @@ interface BaseFieldConfig {
|
|
|
119
47
|
translatable?: boolean;
|
|
120
48
|
indexed?: boolean;
|
|
121
49
|
unique?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Marks the field as **system-managed**: stored as a real column, populated
|
|
52
|
+
* by behavior hooks or trusted server-side transitions, but NOT writable
|
|
53
|
+
* through the public `AdminClient.create/update` surface.
|
|
54
|
+
*
|
|
55
|
+
* - Caller-supplied values for internal fields are silently stripped before
|
|
56
|
+
* hooks run (so an HTTP PATCH cannot poison a workflow state).
|
|
57
|
+
* - `beforeCreate` / `beforeUpdate` hooks may still set them (they run after
|
|
58
|
+
* the strip), and the values are preserved through validation.
|
|
59
|
+
* - Trusted server code that needs to write internals directly — e.g.
|
|
60
|
+
* workflow transitions invoked from an authorized admin route — must use
|
|
61
|
+
* `AdminClient.updateInternal()`, which bypasses the strip.
|
|
62
|
+
*
|
|
63
|
+
* Use this flag on any field added by a behavior whose value represents a
|
|
64
|
+
* controlled state machine (e.g. `_workflowStatus`), not user input.
|
|
65
|
+
*/
|
|
66
|
+
internal?: boolean;
|
|
122
67
|
access?: {
|
|
123
68
|
view?: string;
|
|
124
69
|
edit?: string;
|
|
@@ -199,12 +144,138 @@ interface BlocksField extends BaseFieldConfig {
|
|
|
199
144
|
}
|
|
200
145
|
type FieldConfig = IdField | TextField | NumberField | BooleanField | DateField | SelectField | ReferenceField | MediaField | RichTextField | SlugField | JsonField | BlocksField;
|
|
201
146
|
//#endregion
|
|
147
|
+
//#region src/types/infer.d.ts
|
|
148
|
+
/**
|
|
149
|
+
* Recursive JSON-compatible value, for `field.json()` (jsonb column).
|
|
150
|
+
* Mirrors what Postgres jsonb can store: primitives, arrays, or objects.
|
|
151
|
+
*/
|
|
152
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
153
|
+
[key: string]: JsonValue;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Maps a single FieldConfig to its TypeScript output type.
|
|
157
|
+
* Each branch is a shallow comparison — no recursion.
|
|
158
|
+
*/
|
|
159
|
+
type FieldToTS<F extends FieldConfig> = F extends IdField ? string : F extends TextField ? string : F extends NumberField ? number : F extends BooleanField ? boolean : F extends DateField ? Date | string : F extends SelectField ? F['options'][number] : F extends ReferenceField ? F['cardinality'] extends 'many' ? string[] : string : F extends MediaField ? string : F extends RichTextField ? Record<string, unknown>[] : F extends SlugField ? string : F extends JsonField ? JsonValue : F extends BlocksField ? Array<{
|
|
160
|
+
_block: string;
|
|
161
|
+
_id: string;
|
|
162
|
+
[key: string]: unknown;
|
|
163
|
+
}> : never;
|
|
164
|
+
/**
|
|
165
|
+
* Maps a full field record to its TypeScript output type.
|
|
166
|
+
* Required fields are non-nullable; optional fields are `T | null | undefined`.
|
|
167
|
+
*
|
|
168
|
+
* The `id` field is always `string` and always present.
|
|
169
|
+
* The `id` key from Fields is excluded to avoid duplication since
|
|
170
|
+
* we hardcode `{ id: string }` at the front.
|
|
171
|
+
*/
|
|
172
|
+
type InferEntityDTO<Fields extends Record<string, FieldConfig>> = {
|
|
173
|
+
id: string;
|
|
174
|
+
} & { [K in keyof Fields as K extends 'id' ? never : Fields[K]['required'] extends true ? K : never]: FieldToTS<Fields[K]> } & { [K in keyof Fields as Fields[K]['required'] extends true ? never : K]?: FieldToTS<Fields[K]> | null };
|
|
175
|
+
/** Fields that are auto-generated and should not appear in create input. */
|
|
176
|
+
type AutoGeneratedFields = 'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | '_version';
|
|
177
|
+
/**
|
|
178
|
+
* The input type for creating an entity.
|
|
179
|
+
* - Omits auto-generated fields (id, timestamps, version)
|
|
180
|
+
* - Required fields stay required; optional fields stay optional
|
|
181
|
+
*/
|
|
182
|
+
type InferCreateInput<Fields extends Record<string, FieldConfig>> = Omit<{ [K in keyof Fields as K extends 'id' ? never : Fields[K]['required'] extends true ? K : never]: FieldToTS<Fields[K]> } & { [K in keyof Fields as Fields[K]['required'] extends true ? never : K]?: FieldToTS<Fields[K]> | null }, AutoGeneratedFields>;
|
|
183
|
+
/** Fields that cannot be changed after creation. */
|
|
184
|
+
type ImmutableFields = 'id' | 'createdAt' | 'createdBy';
|
|
185
|
+
type InferUpdateInput<Fields extends Record<string, FieldConfig>> = { [K in keyof Fields as K extends ImmutableFields ? never : K]?: FieldToTS<Fields[K]> | null };
|
|
186
|
+
//#endregion
|
|
202
187
|
//#region src/behaviors/types.d.ts
|
|
188
|
+
/**
|
|
189
|
+
* Structural shape of a queue `JobDefinition` as seen by behaviors.
|
|
190
|
+
*
|
|
191
|
+
* The entity package CANNOT import `@murumets-ee/queue` (would form a
|
|
192
|
+
* cycle through `@murumets-ee/core`'s registry — see CLAUDE.md
|
|
193
|
+
* "Package boundaries"). Instead, behaviors receive an `EnqueueOnCommit`
|
|
194
|
+
* function via `BehaviorContext`, and the queue's `JobDefinition<T>`
|
|
195
|
+
* is structurally compatible with this minimal shape. The actual
|
|
196
|
+
* payload validation runs inside the queue when the wrapped resolver
|
|
197
|
+
* forwards the call.
|
|
198
|
+
*/
|
|
199
|
+
interface JobLike {
|
|
200
|
+
readonly name: string;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Resolver shape for `BehaviorContext.enqueueOnCommit`. Synchronous,
|
|
204
|
+
* fire-and-forget — the resolver writes the row in the background and
|
|
205
|
+
* logs failures; behaviors do NOT await.
|
|
206
|
+
*
|
|
207
|
+
* Intended semantics (PLAN-OUTBOX §4.2): the row INSERT participates
|
|
208
|
+
* in the AdminClient operation's transaction — commit makes the job
|
|
209
|
+
* visible to the worker, rollback removes it. As of PR D this is the
|
|
210
|
+
* actual semantics: AdminClient threads its tx into the
|
|
211
|
+
* `EnqueueOnCommitFactory` once per call and passes the result through
|
|
212
|
+
* to behaviors. When the queue plugin is not loaded the field is
|
|
213
|
+
* `undefined` and behaviors no-op (per §6 Q6).
|
|
214
|
+
*/
|
|
215
|
+
type EnqueueOnCommit = (job: JobLike, payload: unknown) => void;
|
|
216
|
+
/**
|
|
217
|
+
* Factory that returns an {@link EnqueueOnCommit} resolver bound to an
|
|
218
|
+
* optional Drizzle transaction handle. AdminClient invokes this once
|
|
219
|
+
* per CRUD operation, passing the active tx so behaviors firing inside
|
|
220
|
+
* the operation enqueue rows that commit (or roll back) atomically
|
|
221
|
+
* with the entity write.
|
|
222
|
+
*
|
|
223
|
+
* The `tx` parameter is intentionally typed as `unknown` here — the
|
|
224
|
+
* entity package CANNOT import `drizzle-orm/postgres-js` types without
|
|
225
|
+
* pulling in the database stack and breaking its leaf-package contract.
|
|
226
|
+
* The queue plugin (which writes the slot) and AdminClient (which
|
|
227
|
+
* supplies the tx) both work with the precise `PostgresJsDatabase`
|
|
228
|
+
* shape; the interior of the factory casts as needed.
|
|
229
|
+
*
|
|
230
|
+
* **Mirror definition** lives in
|
|
231
|
+
* `packages/queue/src/enqueue-on-commit-slot.ts` with `tx?:
|
|
232
|
+
* PostgresJsDatabase`. The two are structurally compatible by design;
|
|
233
|
+
* if you rename or change one, change both. The runtime contract is
|
|
234
|
+
* the same — only the type-time visibility of the parameter differs.
|
|
235
|
+
*/
|
|
236
|
+
type EnqueueOnCommitFactory = (tx?: unknown) => EnqueueOnCommit;
|
|
203
237
|
/**
|
|
204
238
|
* Context passed to behavior hooks. Resolved once per request by AdminClient
|
|
205
239
|
* from its `contextResolver` and forwarded into every hook so behaviors never
|
|
206
240
|
* have to reach into AsyncLocalStorage themselves — that pattern breaks under
|
|
207
241
|
* bundlers (e.g. Turbopack) that duplicate module instances across boundaries.
|
|
242
|
+
*
|
|
243
|
+
* `loadCurrent` returns the *pre-update* entity row. It is eagerly loaded
|
|
244
|
+
* by AdminClient before any hooks fire on the **update** codepath and
|
|
245
|
+
* cached for the rest of that call — so calling it from `beforeUpdate`
|
|
246
|
+
* or `afterUpdate` returns the SAME snapshot regardless of whether a
|
|
247
|
+
* sibling hook touched it. Returns `null` when no entity matches the id
|
|
248
|
+
* (rare — usually means the row was deleted concurrently). On `create`
|
|
249
|
+
* AND `delete` codepaths, `loadCurrent` is undefined: create has no
|
|
250
|
+
* pre-state, and delete hooks receive the entity id directly via their
|
|
251
|
+
* first argument so a separate snapshot is unnecessary.
|
|
252
|
+
*
|
|
253
|
+
* `afterUpdate` always receives the post-update row as its first argument,
|
|
254
|
+
* so the (`row`, `loadCurrent()`) pair gives hooks a complete (after, before)
|
|
255
|
+
* view without per-call Map state. The eager load costs one extra
|
|
256
|
+
* `findById` per update; updates are not a hot path in this codebase, and
|
|
257
|
+
* the consistency win — no foot-gun for hook authors — is worth it.
|
|
258
|
+
*
|
|
259
|
+
* `viaInternal` is `true` when the hook is running on an `AdminClient.updateInternal`
|
|
260
|
+
* call (the trusted server-side path; public PATCH always sets it false).
|
|
261
|
+
* Hooks SHOULD treat this as informational only — the route layer has
|
|
262
|
+
* authorized the *capability*, but the hook is still responsible for
|
|
263
|
+
* enforcing structural invariants. Workflowable, for example, validates
|
|
264
|
+
* the `_workflowStatus` transition table on every update regardless of
|
|
265
|
+
* `viaInternal` so that even a route-layer bug cannot push the workflow
|
|
266
|
+
* row into an illegal state. Use `viaInternal` when a hook genuinely
|
|
267
|
+
* needs to differentiate (e.g. side effects that should only fire when
|
|
268
|
+
* a real user — not the seed loader — initiates the change).
|
|
269
|
+
*
|
|
270
|
+
* `enqueueOnCommit` is the outbox entry point for projection-style hooks
|
|
271
|
+
* (PR C of PLAN-OUTBOX). When wired (the queue plugin is available in
|
|
272
|
+
* the running app), behaviors can enqueue side-effect jobs without
|
|
273
|
+
* importing the queue package directly, keeping entity-as-leaf invariant
|
|
274
|
+
* intact. Synchronous + fire-and-forget — no await needed in hooks.
|
|
275
|
+
* Undefined when the AdminClient was constructed without an
|
|
276
|
+
* `enqueueOnCommit` resolver (e.g. CLI scripts that don't load the
|
|
277
|
+
* queue plugin); behaviors that depend on it must guard with `if
|
|
278
|
+
* (ctx.enqueueOnCommit) { ... }` or document the dependency loudly.
|
|
208
279
|
*/
|
|
209
280
|
interface BehaviorContext {
|
|
210
281
|
user?: {
|
|
@@ -212,6 +283,9 @@ interface BehaviorContext {
|
|
|
212
283
|
name?: string;
|
|
213
284
|
email?: string;
|
|
214
285
|
};
|
|
286
|
+
loadCurrent?: () => Promise<Record<string, unknown> | null>;
|
|
287
|
+
viaInternal?: boolean;
|
|
288
|
+
enqueueOnCommit?: EnqueueOnCommit;
|
|
215
289
|
}
|
|
216
290
|
interface Behavior<F extends Record<string, FieldConfig> = {}> {
|
|
217
291
|
name: string;
|
|
@@ -226,6 +300,78 @@ interface Behavior<F extends Record<string, FieldConfig> = {}> {
|
|
|
226
300
|
};
|
|
227
301
|
}
|
|
228
302
|
//#endregion
|
|
303
|
+
//#region src/cursor.d.ts
|
|
304
|
+
/** Cursor input for keyset pagination. */
|
|
305
|
+
interface CursorInput {
|
|
306
|
+
/** Sort field name (e.g. 'createdAt'). Must be a real column on the entity. */
|
|
307
|
+
field: string;
|
|
308
|
+
/** Last seen value of the sort field. */
|
|
309
|
+
value: string | number;
|
|
310
|
+
/** Sort direction — must match the ORDER BY direction. */
|
|
311
|
+
direction: 'asc' | 'desc';
|
|
312
|
+
/** Tie-breaker: last seen entity ID. Required for non-unique sort fields. */
|
|
313
|
+
id?: string | undefined;
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/admin-config.d.ts
|
|
317
|
+
/**
|
|
318
|
+
* Optional admin UI configuration for entities.
|
|
319
|
+
* Controls how entities appear in the admin sidebar, list pages, and forms.
|
|
320
|
+
*/
|
|
321
|
+
interface EntityAdminConfig {
|
|
322
|
+
/** Sidebar section: 'content' | 'structure' | custom string. Default: 'content' */
|
|
323
|
+
group?: string;
|
|
324
|
+
/** Plural display name for sidebar + list pages. Default: title-cased pluralized entity name */
|
|
325
|
+
label?: string;
|
|
326
|
+
/** Singular label for "New X" button. Default: title-cased entity name */
|
|
327
|
+
labelSingular?: string;
|
|
328
|
+
/** Lucide icon name as string, e.g. 'file-text' */
|
|
329
|
+
icon?: string;
|
|
330
|
+
/** Description shown on list page */
|
|
331
|
+
description?: string;
|
|
332
|
+
/** Form layout. Default: 'single' */
|
|
333
|
+
layout?: 'single' | 'two-column';
|
|
334
|
+
/** Fields to hide in the form */
|
|
335
|
+
hiddenFields?: string[];
|
|
336
|
+
/** Columns to hide in the list */
|
|
337
|
+
hiddenColumns?: string[];
|
|
338
|
+
/** Per-field label/description/placeholder overrides */
|
|
339
|
+
fieldOverrides?: Record<string, {
|
|
340
|
+
label?: string;
|
|
341
|
+
description?: string;
|
|
342
|
+
placeholder?: string;
|
|
343
|
+
}>;
|
|
344
|
+
/** Default list sort field. Default: 'createdAt' */
|
|
345
|
+
defaultSort?: string;
|
|
346
|
+
/** Default list sort direction. Default: 'desc' */
|
|
347
|
+
defaultSortDirection?: 'asc' | 'desc';
|
|
348
|
+
/** List page size. Default: 20 */
|
|
349
|
+
pageSize?: number;
|
|
350
|
+
/** For block editor: fields to show in Puck root config */
|
|
351
|
+
rootFields?: string[];
|
|
352
|
+
/** Suppress "New" button in the list page */
|
|
353
|
+
disableCreate?: boolean;
|
|
354
|
+
/** Order within sidebar group. Default: 0 */
|
|
355
|
+
sortOrder?: number;
|
|
356
|
+
/**
|
|
357
|
+
* Hide the entity from BOTH dashboard and sidebar.
|
|
358
|
+
* Use for system entities that should not appear in any UI.
|
|
359
|
+
* Shorthand for `hideFromMenu: true` + `hideFromDashboard: true`.
|
|
360
|
+
*/
|
|
361
|
+
hidden?: boolean;
|
|
362
|
+
/**
|
|
363
|
+
* Hide from sidebar menu only. Entity remains accessible via dashboard, direct URL,
|
|
364
|
+
* and the entity API. Use for storage models accessed through specialized UI
|
|
365
|
+
* (e.g., Ticket entities accessed via the inbox/board, not as a CRUD table).
|
|
366
|
+
*/
|
|
367
|
+
hideFromMenu?: boolean;
|
|
368
|
+
/**
|
|
369
|
+
* Hide from dashboard widget grid only. Entity still appears in sidebar.
|
|
370
|
+
* Use for entities that don't have meaningful counts/stats to show.
|
|
371
|
+
*/
|
|
372
|
+
hideFromDashboard?: boolean;
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
229
375
|
//#region src/define-entity.d.ts
|
|
230
376
|
/**
|
|
231
377
|
* A fully resolved entity with merged behavior fields.
|
|
@@ -277,46 +423,6 @@ interface SecurityContext {
|
|
|
277
423
|
*/
|
|
278
424
|
type ContextResolver = () => SecurityContext | undefined | Promise<SecurityContext | undefined>;
|
|
279
425
|
//#endregion
|
|
280
|
-
//#region src/types/infer.d.ts
|
|
281
|
-
/**
|
|
282
|
-
* Recursive JSON-compatible value, for `field.json()` (jsonb column).
|
|
283
|
-
* Mirrors what Postgres jsonb can store: primitives, arrays, or objects.
|
|
284
|
-
*/
|
|
285
|
-
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
286
|
-
[key: string]: JsonValue;
|
|
287
|
-
};
|
|
288
|
-
/**
|
|
289
|
-
* Maps a single FieldConfig to its TypeScript output type.
|
|
290
|
-
* Each branch is a shallow comparison — no recursion.
|
|
291
|
-
*/
|
|
292
|
-
type FieldToTS<F extends FieldConfig> = F extends IdField ? string : F extends TextField ? string : F extends NumberField ? number : F extends BooleanField ? boolean : F extends DateField ? Date | string : F extends SelectField ? F['options'][number] : F extends ReferenceField ? F['cardinality'] extends 'many' ? string[] : string : F extends MediaField ? string : F extends RichTextField ? Record<string, unknown>[] : F extends SlugField ? string : F extends JsonField ? JsonValue : F extends BlocksField ? Array<{
|
|
293
|
-
_block: string;
|
|
294
|
-
_id: string;
|
|
295
|
-
[key: string]: unknown;
|
|
296
|
-
}> : never;
|
|
297
|
-
/**
|
|
298
|
-
* Maps a full field record to its TypeScript output type.
|
|
299
|
-
* Required fields are non-nullable; optional fields are `T | null | undefined`.
|
|
300
|
-
*
|
|
301
|
-
* The `id` field is always `string` and always present.
|
|
302
|
-
* The `id` key from Fields is excluded to avoid duplication since
|
|
303
|
-
* we hardcode `{ id: string }` at the front.
|
|
304
|
-
*/
|
|
305
|
-
type InferEntityDTO<Fields extends Record<string, FieldConfig>> = {
|
|
306
|
-
id: string;
|
|
307
|
-
} & { [K in keyof Fields as K extends 'id' ? never : Fields[K]['required'] extends true ? K : never]: FieldToTS<Fields[K]> } & { [K in keyof Fields as Fields[K]['required'] extends true ? never : K]?: FieldToTS<Fields[K]> | null };
|
|
308
|
-
/** Fields that are auto-generated and should not appear in create input. */
|
|
309
|
-
type AutoGeneratedFields = 'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | '_version';
|
|
310
|
-
/**
|
|
311
|
-
* The input type for creating an entity.
|
|
312
|
-
* - Omits auto-generated fields (id, timestamps, version)
|
|
313
|
-
* - Required fields stay required; optional fields stay optional
|
|
314
|
-
*/
|
|
315
|
-
type InferCreateInput<Fields extends Record<string, FieldConfig>> = Omit<{ [K in keyof Fields as K extends 'id' ? never : Fields[K]['required'] extends true ? K : never]: FieldToTS<Fields[K]> } & { [K in keyof Fields as Fields[K]['required'] extends true ? never : K]?: FieldToTS<Fields[K]> | null }, AutoGeneratedFields>;
|
|
316
|
-
/** Fields that cannot be changed after creation. */
|
|
317
|
-
type ImmutableFields = 'id' | 'createdAt' | 'createdBy';
|
|
318
|
-
type InferUpdateInput<Fields extends Record<string, FieldConfig>> = { [K in keyof Fields as K extends ImmutableFields ? never : K]?: FieldToTS<Fields[K]> | null };
|
|
319
|
-
//#endregion
|
|
320
426
|
//#region src/types/logger.d.ts
|
|
321
427
|
/**
|
|
322
428
|
* Minimal logger interface compatible with Pino.
|
|
@@ -343,32 +449,64 @@ type EntityResolver = () => Map<string, Entity> | undefined;
|
|
|
343
449
|
interface AdminClientConfig<AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>> {
|
|
344
450
|
entity: Entity<AllFields>;
|
|
345
451
|
db: PostgresJsDatabase;
|
|
346
|
-
logger?: Logger;
|
|
452
|
+
logger?: Logger | undefined;
|
|
347
453
|
/** Optional count cache for COUNT(*) query optimization. */
|
|
348
|
-
countCache?: CountCacheLike;
|
|
454
|
+
countCache?: CountCacheLike | undefined;
|
|
349
455
|
/**
|
|
350
456
|
* Resolves the current request's security context (user, role checker, scope).
|
|
351
457
|
* Provided automatically by `createAdminClient()` from @murumets-ee/core/clients.
|
|
352
458
|
* For direct `new AdminClient()` usage, pass your own resolver or use `runAsCli()`.
|
|
353
459
|
*/
|
|
354
|
-
contextResolver?: ContextResolver;
|
|
460
|
+
contextResolver?: ContextResolver | undefined;
|
|
355
461
|
/**
|
|
356
462
|
* Resolves the running app's entity registry. Used by cascade delete to
|
|
357
463
|
* read each referencing field's `onDelete` strategy. When omitted, all
|
|
358
464
|
* incoming references are treated as `restrict` — a safe default that
|
|
359
465
|
* blocks deletes rather than guessing.
|
|
360
466
|
*/
|
|
361
|
-
entityResolver?: EntityResolver;
|
|
467
|
+
entityResolver?: EntityResolver | undefined;
|
|
468
|
+
/**
|
|
469
|
+
* Outbox entry point exposed on `BehaviorContext.enqueueOnCommit`
|
|
470
|
+
* (PR C of PLAN-OUTBOX). When provided, projection-style behaviors can
|
|
471
|
+
* fire side-effect jobs from their hooks without importing the queue
|
|
472
|
+
* package — the entity package stays a leaf in the dependency graph.
|
|
473
|
+
*
|
|
474
|
+
* Used as a fallback when `enqueueOnCommitFactory` is not supplied:
|
|
475
|
+
* AdminClient passes this resolver verbatim into hook contexts, and
|
|
476
|
+
* the row INSERT is NOT bound to the operation's transaction. Tests
|
|
477
|
+
* that need to capture call-site behavior without wiring queue can
|
|
478
|
+
* pass this directly.
|
|
479
|
+
*
|
|
480
|
+
* In production wiring (`createAdminClient` in
|
|
481
|
+
* `@murumets-ee/core/clients`), prefer `enqueueOnCommitFactory` —
|
|
482
|
+
* that's the form that delivers PLAN-OUTBOX §4.2's "rollback removes
|
|
483
|
+
* the job" guarantee.
|
|
484
|
+
*/
|
|
485
|
+
enqueueOnCommit?: BehaviorContext['enqueueOnCommit'] | undefined;
|
|
486
|
+
/**
|
|
487
|
+
* Outbox factory exposed on `BehaviorContext.enqueueOnCommit` (PR D
|
|
488
|
+
* of PLAN-OUTBOX). When provided, AdminClient invokes the factory
|
|
489
|
+
* once per CRUD operation with the active Drizzle transaction so the
|
|
490
|
+
* resulting fire-and-forget resolver routes its INSERT through that
|
|
491
|
+
* tx. Hook-fired enqueues commit (or roll back) atomically with the
|
|
492
|
+
* entity write — the canonical PLAN-OUTBOX §4.2 semantics.
|
|
493
|
+
*
|
|
494
|
+
* Wired by `createAdminClient()` in `@murumets-ee/core/clients`
|
|
495
|
+
* against the running app's queue plugin. Takes precedence over the
|
|
496
|
+
* older `enqueueOnCommit` field when both are set; CLI scripts that
|
|
497
|
+
* don't load the queue plugin omit both.
|
|
498
|
+
*/
|
|
499
|
+
enqueueOnCommitFactory?: EnqueueOnCommitFactory | undefined;
|
|
362
500
|
}
|
|
363
501
|
interface FindManyOptions {
|
|
364
502
|
where?: SQL | undefined;
|
|
365
|
-
limit?: number;
|
|
366
|
-
offset?: number;
|
|
367
|
-
orderBy?: SQL | SQL[];
|
|
368
|
-
locale?: string;
|
|
503
|
+
limit?: number | undefined;
|
|
504
|
+
offset?: number | undefined;
|
|
505
|
+
orderBy?: SQL | SQL[] | undefined;
|
|
506
|
+
locale?: string | undefined;
|
|
369
507
|
/** Default content locale. For localized blocks, NULL rows (from initial create)
|
|
370
508
|
* are only returned as fallback when locale matches defaultLocale. */
|
|
371
|
-
defaultLocale?: string;
|
|
509
|
+
defaultLocale?: string | undefined;
|
|
372
510
|
/**
|
|
373
511
|
* Cursor-based (keyset) pagination. When provided, replaces OFFSET with a
|
|
374
512
|
* WHERE condition for O(1) page access at any depth. The `offset` option
|
|
@@ -376,7 +514,13 @@ interface FindManyOptions {
|
|
|
376
514
|
*
|
|
377
515
|
* The cursor `field` must be a real column on the entity table.
|
|
378
516
|
*/
|
|
379
|
-
cursor?: CursorInput;
|
|
517
|
+
cursor?: CursorInput | undefined;
|
|
518
|
+
/**
|
|
519
|
+
* Include fields marked `internal: true` (and legacy `_`-prefixed
|
|
520
|
+
* infrastructure columns) in the returned DTOs. Use only on trusted
|
|
521
|
+
* server-side reads that need to inspect state-machine fields.
|
|
522
|
+
*/
|
|
523
|
+
includeInternal?: boolean | undefined;
|
|
380
524
|
}
|
|
381
525
|
interface CountOptions {
|
|
382
526
|
where?: SQL | undefined;
|
|
@@ -406,17 +550,84 @@ declare class AdminClient<AllFields extends Record<string, FieldConfig> = Record
|
|
|
406
550
|
private entity;
|
|
407
551
|
private db;
|
|
408
552
|
private logger?;
|
|
553
|
+
/** Public-surface create schema — internal fields excluded. */
|
|
409
554
|
private createSchema;
|
|
555
|
+
/** Public-surface update schema — internal fields excluded. */
|
|
410
556
|
private updateSchema;
|
|
557
|
+
/** Trusted-surface update schema — internal fields included. Used by `updateInternal()`. */
|
|
558
|
+
private updateInternalSchema;
|
|
559
|
+
/** Names of fields marked `internal: true`. Cached for O(1) strip / pick. */
|
|
560
|
+
private internalFieldNames;
|
|
411
561
|
private table;
|
|
412
562
|
private countCache?;
|
|
413
563
|
private contextResolver?;
|
|
414
564
|
private entityResolver?;
|
|
415
|
-
|
|
565
|
+
private enqueueOnCommit?;
|
|
566
|
+
private enqueueOnCommitFactory?;
|
|
567
|
+
/** Shared context for entity-data-ops functions, bound to the default db. */
|
|
416
568
|
private get ctx();
|
|
569
|
+
/**
|
|
570
|
+
* Build an {@link EntityContext} bound to a specific Drizzle executor.
|
|
571
|
+
*
|
|
572
|
+
* Used by the public CRUD methods so that when the caller threads in
|
|
573
|
+
* `{ tx }` (PR D of PLAN-OUTBOX) every block-load / translation
|
|
574
|
+
* merge / scope-condition lookup hits the SAME tx as the entity write.
|
|
575
|
+
* Without this, a `loadBlocks(this.ctx, ...)` call inside an active tx
|
|
576
|
+
* would query `this.db` (the AdminClient's owned connection) and miss
|
|
577
|
+
* the in-flight INSERT — `entity_refs` / blocks rows wouldn't be
|
|
578
|
+
* visible until commit.
|
|
579
|
+
*
|
|
580
|
+
* For callers that don't pass a tx, `exec === this.db` and the ctx
|
|
581
|
+
* matches today's behaviour exactly.
|
|
582
|
+
*/
|
|
583
|
+
private ctxFor;
|
|
584
|
+
/**
|
|
585
|
+
* Build the `enqueueOnCommit` slot of a {@link BehaviorContext} for
|
|
586
|
+
* the active tx, ready to spread into `resolveAuthContext`'s options
|
|
587
|
+
* (PR D of PLAN-OUTBOX).
|
|
588
|
+
*
|
|
589
|
+
* - When the queue plugin's factory is wired, invoke it with `tx` so
|
|
590
|
+
* the resulting fire-and-forget resolver routes its INSERT through
|
|
591
|
+
* the caller's transaction (atomic with the entity write).
|
|
592
|
+
* - When only the legacy static `enqueueOnCommit` option is set, pass
|
|
593
|
+
* it through verbatim — used by tests and callers that wired their
|
|
594
|
+
* own resolver directly.
|
|
595
|
+
* - When neither is configured (CLI scripts, no queue plugin), return
|
|
596
|
+
* `{}` so the BehaviorContext's `enqueueOnCommit` field stays
|
|
597
|
+
* undefined and behaviors guard accordingly.
|
|
598
|
+
*/
|
|
599
|
+
private buildAuthOptions;
|
|
600
|
+
/**
|
|
601
|
+
* Re-bind `behaviorCtx.enqueueOnCommit` to the supplied executor.
|
|
602
|
+
*
|
|
603
|
+
* Used by `delete` / `deleteMany` when they open an internal tx
|
|
604
|
+
* because no caller tx was supplied: the auth context was resolved
|
|
605
|
+
* BEFORE the tx existed, so its `enqueueOnCommit` (if a factory is
|
|
606
|
+
* wired) was bound to `undefined` and would commit the queue row
|
|
607
|
+
* outside the internal tx. Rebinding here ensures the queue write
|
|
608
|
+
* rolls back with the cascade if anything in `runDelete` throws.
|
|
609
|
+
*
|
|
610
|
+
* Spreads conditionally to satisfy `exactOptionalPropertyTypes` —
|
|
611
|
+
* never assigns `enqueueOnCommit: undefined` (which would clear an
|
|
612
|
+
* existing static resolver if the factory wasn't configured).
|
|
613
|
+
*/
|
|
614
|
+
private rebindEnqueueOnCommit;
|
|
417
615
|
constructor(config: AdminClientConfig<AllFields>);
|
|
418
616
|
/**
|
|
419
|
-
*
|
|
617
|
+
* Strip caller-provided values for internal fields from the input.
|
|
618
|
+
*
|
|
619
|
+
* Returns a NEW object — does not mutate the caller's input. Hooks run
|
|
620
|
+
* AFTER this strip, so they can still set internal fields legitimately;
|
|
621
|
+
* those values are then preserved through validation by `pickInternalFields`.
|
|
622
|
+
*/
|
|
623
|
+
private stripCallerInternals;
|
|
624
|
+
/**
|
|
625
|
+
* Pick the internal-field values that hooks set on `data`.
|
|
626
|
+
* Used to re-attach them after schema validation strips unknown keys.
|
|
627
|
+
*/
|
|
628
|
+
private pickInternalFields;
|
|
629
|
+
/**
|
|
630
|
+
* Create a new entity.
|
|
420
631
|
*
|
|
421
632
|
* Flow:
|
|
422
633
|
* 1. Validate input with Zod
|
|
@@ -425,14 +636,37 @@ declare class AdminClient<AllFields extends Record<string, FieldConfig> = Record
|
|
|
425
636
|
* 4. Insert into database
|
|
426
637
|
* 5. Execute afterCreate hooks
|
|
427
638
|
* 6. Shape DTO and return
|
|
639
|
+
*
|
|
640
|
+
* **Caller transaction (`options.tx`)** — PR D of PLAN-OUTBOX. When
|
|
641
|
+
* supplied, the row INSERT, blocks save, refs sync, and any hook-fired
|
|
642
|
+
* `enqueueOnCommit` participate in the caller's transaction:
|
|
643
|
+
*
|
|
644
|
+
* ```ts
|
|
645
|
+
* await db.transaction(async (tx) => {
|
|
646
|
+
* const order = await orders.create({ ... }, { tx })
|
|
647
|
+
* // afterCreate hooks fired with this tx — enqueueOnCommit's INSERT
|
|
648
|
+
* // routes through it. Outer rollback removes both atomically.
|
|
649
|
+
* })
|
|
650
|
+
* ```
|
|
651
|
+
*
|
|
652
|
+
* When omitted, behaviour matches pre-PR-D exactly: each statement
|
|
653
|
+
* auto-commits, hooks run after commit, and the static
|
|
654
|
+
* `enqueueOnCommit` resolver (if any) writes outside the entity tx.
|
|
428
655
|
*/
|
|
429
|
-
create(data: InferCreateInput<AllFields
|
|
656
|
+
create(data: InferCreateInput<AllFields>, options?: {
|
|
657
|
+
tx?: PostgresJsDatabase;
|
|
658
|
+
}): Promise<InferEntityDTO<AllFields>>;
|
|
430
659
|
/**
|
|
431
|
-
* Find entity by ID
|
|
660
|
+
* Find entity by ID.
|
|
661
|
+
*
|
|
662
|
+
* Pass `includeInternal: true` from trusted server code (workflow
|
|
663
|
+
* transitions, behavior implementations) when you need to read fields
|
|
664
|
+
* marked `internal: true` — by default they are stripped from the DTO.
|
|
432
665
|
*/
|
|
433
666
|
findById(id: string, options?: {
|
|
434
667
|
locale?: string;
|
|
435
668
|
defaultLocale?: string;
|
|
669
|
+
includeInternal?: boolean;
|
|
436
670
|
}): Promise<InferEntityDTO<AllFields> | null>;
|
|
437
671
|
/**
|
|
438
672
|
* Find multiple entities
|
|
@@ -455,17 +689,65 @@ declare class AdminClient<AllFields extends Record<string, FieldConfig> = Record
|
|
|
455
689
|
*/
|
|
456
690
|
update(id: string, data: InferUpdateInput<AllFields>, options?: {
|
|
457
691
|
locale?: string;
|
|
692
|
+
tx?: PostgresJsDatabase;
|
|
693
|
+
}): Promise<InferEntityDTO<AllFields>>;
|
|
694
|
+
/**
|
|
695
|
+
* Update entity by ID, allowing writes to fields marked `internal: true`.
|
|
696
|
+
*
|
|
697
|
+
* Use this from trusted server code that has already authorized the
|
|
698
|
+
* transition out-of-band — typical example: workflow transitions invoked
|
|
699
|
+
* from an admin route that has already checked the `publish` permission.
|
|
700
|
+
* The HTTP `PATCH` surface uses the public {@link update} method, which
|
|
701
|
+
* silently strips internal fields so untrusted callers cannot poison
|
|
702
|
+
* state-machine values like `_workflowStatus`.
|
|
703
|
+
*
|
|
704
|
+
* The validation schema for this method INCLUDES internal fields — values
|
|
705
|
+
* are still type-checked and constrained (e.g. select-field options).
|
|
706
|
+
*
|
|
707
|
+
* **Security**: never call this from a code path that forwards request body
|
|
708
|
+
* fields directly. The caller must construct the internal-field values
|
|
709
|
+
* server-side from authorized state transitions.
|
|
710
|
+
*/
|
|
711
|
+
updateInternal(id: string, data: Record<string, unknown>, options?: {
|
|
712
|
+
locale?: string;
|
|
713
|
+
tx?: PostgresJsDatabase;
|
|
458
714
|
}): Promise<InferEntityDTO<AllFields>>;
|
|
715
|
+
private updateImpl;
|
|
716
|
+
/**
|
|
717
|
+
* Internal `findById` variant that issues its SELECT through the
|
|
718
|
+
* caller-supplied executor. Used by `updateImpl` so the pre-update
|
|
719
|
+
* snapshot reflects any earlier writes the caller already made
|
|
720
|
+
* inside the same transaction.
|
|
721
|
+
*
|
|
722
|
+
* **Scope is NOT optional** — it must be the same `scopeId` that
|
|
723
|
+
* `resolveAuthContext` produced for the surrounding `update` call.
|
|
724
|
+
* The eventual UPDATE is scope-gated via its WHERE clause; if this
|
|
725
|
+
* pre-load skipped scope, hooks with side effects (audit logs,
|
|
726
|
+
* system-message creation, outbox enqueues) would observe and
|
|
727
|
+
* dispatch on cross-tenant rows even when the gated UPDATE matches
|
|
728
|
+
* nothing. See HIGH finding in pr-review for #247.
|
|
729
|
+
*
|
|
730
|
+
* Permission check is skipped because `resolveAuthContext` ran first
|
|
731
|
+
* in the surrounding `update` call.
|
|
732
|
+
*/
|
|
733
|
+
private findByIdInternal;
|
|
459
734
|
/**
|
|
460
735
|
* Delete entity by ID.
|
|
461
736
|
*
|
|
462
737
|
* Wraps cascade + delete in a transaction so cascaded deletes are rolled
|
|
463
|
-
* back if the final entity delete fails (no partial data loss).
|
|
738
|
+
* back if the final entity delete fails (no partial data loss). When
|
|
739
|
+
* the caller threads in `{ tx }` (PR D of PLAN-OUTBOX), the same tx
|
|
740
|
+
* is reused — the inner `db.transaction(...)` call is replaced with a
|
|
741
|
+
* direct invocation of the body, and any hook-fired `enqueueOnCommit`
|
|
742
|
+
* INSERT routes through the caller's tx so an outer rollback removes
|
|
743
|
+
* everything atomically.
|
|
464
744
|
*
|
|
465
745
|
* Checks entity_refs for incoming references first — throws
|
|
466
746
|
* ReferencedEntityError if this entity is still used somewhere.
|
|
467
747
|
*/
|
|
468
|
-
delete(id: string
|
|
748
|
+
delete(id: string, options?: {
|
|
749
|
+
tx?: PostgresJsDatabase;
|
|
750
|
+
}): Promise<void>;
|
|
469
751
|
/**
|
|
470
752
|
* Delete multiple entities matching a WHERE condition.
|
|
471
753
|
*
|
|
@@ -481,7 +763,9 @@ declare class AdminClient<AllFields extends Record<string, FieldConfig> = Record
|
|
|
481
763
|
*
|
|
482
764
|
* @returns Number of rows deleted
|
|
483
765
|
*/
|
|
484
|
-
deleteMany(where: SQL
|
|
766
|
+
deleteMany(where: SQL, options?: {
|
|
767
|
+
tx?: PostgresJsDatabase;
|
|
768
|
+
}): Promise<number>;
|
|
485
769
|
/** Maximum cascade depth to prevent infinite recursion from circular references. */
|
|
486
770
|
private static readonly MAX_CASCADE_DEPTH;
|
|
487
771
|
/**
|
|
@@ -545,6 +829,8 @@ declare class AdminClient<AllFields extends Record<string, FieldConfig> = Record
|
|
|
545
829
|
*/
|
|
546
830
|
updateMany(where: SQL, data: Partial<InferUpdateInput<AllFields>>, options?: {
|
|
547
831
|
expressions?: Record<string, SQL>;
|
|
832
|
+
allowInternal?: boolean;
|
|
833
|
+
tx?: PostgresJsDatabase;
|
|
548
834
|
}): Promise<number>;
|
|
549
835
|
/**
|
|
550
836
|
* Run an aggregate query on this entity's table.
|
|
@@ -618,30 +904,66 @@ declare class AdminClient<AllFields extends Record<string, FieldConfig> = Record
|
|
|
618
904
|
/**
|
|
619
905
|
* Save translation for an entity.
|
|
620
906
|
* Each translatable field is a real column on the translation table.
|
|
907
|
+
*
|
|
908
|
+
* **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX. When
|
|
909
|
+
* supplied, the upsert participates in the caller's transaction so a
|
|
910
|
+
* caller wrapping `entity.create + saveTranslation` in a `db.transaction`
|
|
911
|
+
* gets atomic visibility — outer rollback removes BOTH the entity row
|
|
912
|
+
* and the translation row, never leaves an orphan translation pointing
|
|
913
|
+
* at a now-deleted entity. Without it, the translation would auto-commit
|
|
914
|
+
* regardless of the outer rollback. No hooks fire on this method, so no
|
|
915
|
+
* factory plumbing is needed — just routing the SQL through `exec`.
|
|
621
916
|
*/
|
|
622
|
-
saveTranslation(entityId: string, locale: string, translations: Record<string, unknown
|
|
917
|
+
saveTranslation(entityId: string, locale: string, translations: Record<string, unknown>, options?: {
|
|
918
|
+
tx?: PostgresJsDatabase;
|
|
919
|
+
}): Promise<void>;
|
|
623
920
|
/**
|
|
624
|
-
*
|
|
625
|
-
*
|
|
921
|
+
* Delete translation(s) for an entity. If `locale` is provided, deletes
|
|
922
|
+
* the specific locale row; otherwise deletes every translation row for
|
|
923
|
+
* the entity.
|
|
924
|
+
*
|
|
925
|
+
* **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX. Same
|
|
926
|
+
* shape as `saveTranslation` — when supplied, the DELETE participates
|
|
927
|
+
* in the caller's tx.
|
|
626
928
|
*/
|
|
627
|
-
deleteTranslation(entityId: string, locale?: string
|
|
929
|
+
deleteTranslation(entityId: string, locale?: string, options?: {
|
|
930
|
+
tx?: PostgresJsDatabase;
|
|
931
|
+
}): Promise<void>;
|
|
628
932
|
/**
|
|
629
933
|
* Save block-level translations for an entity.
|
|
630
934
|
* For each block in each blocks field, extracts translatable field values
|
|
631
935
|
* and upserts them into {entity}_layout_translations.
|
|
632
936
|
*
|
|
633
937
|
* Block _id must match an existing layout row ID.
|
|
938
|
+
*
|
|
939
|
+
* **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX.
|
|
940
|
+
* Routes the layout-row lookup AND every layout-translation upsert
|
|
941
|
+
* through the supplied tx, so a caller doing
|
|
942
|
+
* `entity.update + saveBlockTranslations` in one tx gets atomic
|
|
943
|
+
* visibility on rollback.
|
|
634
944
|
*/
|
|
635
|
-
saveBlockTranslations(entityId: string, locale: string, data: Record<string, unknown
|
|
945
|
+
saveBlockTranslations(entityId: string, locale: string, data: Record<string, unknown>, options?: {
|
|
946
|
+
tx?: PostgresJsDatabase;
|
|
947
|
+
}): Promise<void>;
|
|
636
948
|
/**
|
|
637
949
|
* Save per-locale blocks for an entity.
|
|
638
950
|
* Used for entities with `localized: true` on their blocks field — each locale gets
|
|
639
951
|
* its own independent block layout rows in the layout table.
|
|
952
|
+
*
|
|
953
|
+
* **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX. Threaded
|
|
954
|
+
* through to `saveBlocks`, which already accepts an executor.
|
|
640
955
|
*/
|
|
641
|
-
saveLocalizedBlocks(entityId: string, locale: string, data: Record<string, unknown
|
|
956
|
+
saveLocalizedBlocks(entityId: string, locale: string, data: Record<string, unknown>, options?: {
|
|
957
|
+
tx?: PostgresJsDatabase;
|
|
958
|
+
}): Promise<void>;
|
|
642
959
|
/**
|
|
643
960
|
* Save blocks for an entity after create/update.
|
|
644
961
|
* For each blocks field, writes rows to {entity}_layout table.
|
|
962
|
+
*
|
|
963
|
+
* `exec` is the active Drizzle executor — either a caller-supplied tx
|
|
964
|
+
* (PR D of PLAN-OUTBOX) or `this.db`. Routing through it keeps the
|
|
965
|
+
* blocks INSERTs/DELETEs in the same transaction as the parent
|
|
966
|
+
* entity write.
|
|
645
967
|
*/
|
|
646
968
|
private saveBlocks;
|
|
647
969
|
/**
|
|
@@ -651,13 +973,27 @@ declare class AdminClient<AllFields extends Record<string, FieldConfig> = Record
|
|
|
651
973
|
* - 'update': deletes refs for changed ref-bearing fields, then inserts new ones
|
|
652
974
|
*
|
|
653
975
|
* Gracefully skips if entity_refs table is not registered (e.g. before migration).
|
|
976
|
+
*
|
|
977
|
+
* `exec` is the active Drizzle executor — caller-supplied tx (PR D of
|
|
978
|
+
* PLAN-OUTBOX) or `this.db`. Routes the entity_refs UPDATE/INSERT
|
|
979
|
+
* through the same transaction as the parent entity write.
|
|
654
980
|
*/
|
|
655
981
|
private syncRefs;
|
|
656
982
|
/**
|
|
657
983
|
* Invalidate all count cache entries for this entity.
|
|
984
|
+
*
|
|
985
|
+
* **Tx-aware** (PR D of PLAN-OUTBOX). When `inCallerTx === true`, we
|
|
986
|
+
* skip the invalidation: the caller's tx hasn't committed yet, and
|
|
987
|
+
* eagerly invalidating would have other connections re-compute the
|
|
988
|
+
* count from the still-pre-tx state and cache that stale value.
|
|
989
|
+
* The caller is responsible for invalidating after their own commit
|
|
990
|
+
* (or accepting the cache TTL self-heal). For internally-managed
|
|
991
|
+
* txs (no-caller-tx path) and no-tx CRUD calls, the invalidation
|
|
992
|
+
* runs after the inner `db.transaction(...)` resolves — which only
|
|
993
|
+
* happens post-commit — so the cache reflects the committed state.
|
|
658
994
|
*/
|
|
659
995
|
private invalidateCountCache;
|
|
660
996
|
}
|
|
661
997
|
//#endregion
|
|
662
|
-
export { AdminClient, type AdminClientConfig, type CountCacheLike, type CountOptions, type FindManyOptions };
|
|
998
|
+
export { AdminClient, type AdminClientConfig, type CountCacheLike, type CountOptions, type FindManyOptions, type InferCreateInput, type InferEntityDTO, type InferUpdateInput };
|
|
663
999
|
//# sourceMappingURL=index.d.mts.map
|