@spfn/core 0.3.0-beta.5 → 0.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -4
- package/dist/db/index.d.ts +173 -27
- package/dist/db/index.js +192 -57
- package/dist/db/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +18 -1
- package/dist/nextjs/index.js +40 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +34 -1
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/docs/file-upload.md +195 -333
- package/package.json +6 -5
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +344 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +736 -0
- package/src/job/README.md +514 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
package/README.md
CHANGED
|
@@ -185,18 +185,144 @@ import type { AppRouter } from '@/server/router';
|
|
|
185
185
|
|
|
186
186
|
export const api = createApi<AppRouter>();
|
|
187
187
|
|
|
188
|
-
//
|
|
188
|
+
// the same client in a Server Component, a Client Component or a Server Action:
|
|
189
189
|
const user = await api.getUser.call({ params: { id: '123' } }); // typed { id, name }
|
|
190
190
|
const made = await api.createUser.call({ body: { name: 'A' } });
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
+
That the client is isomorphic does not make the three callers interchangeable. **A page's
|
|
194
|
+
initial data is awaited in the Server Component**, where
|
|
195
|
+
`api.getUser.fetchOptions({ next: { revalidate, tags } }).call(…)` participates in
|
|
196
|
+
Next.js caching and reaches the backend without a browser round trip. Fetching that same
|
|
197
|
+
first paint from a `'use client'` component inside a `useEffect` is the anti-pattern: it
|
|
198
|
+
ships a loading state and a second network hop for data the server already had. Client
|
|
199
|
+
Components and Server Actions are for what happens after the first paint — interaction
|
|
200
|
+
and mutation. Cache tags, revalidation and SSR cookie forwarding are in
|
|
201
|
+
[the Next.js bridge docs](https://superfunction.xyz/docs/packages/core/nextjs).
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## How does a repository talk to the database?
|
|
206
|
+
|
|
207
|
+
Through `BaseRepository`. Extending it gives a repository two transaction-aware
|
|
208
|
+
connections — `this.db` (write/primary) and `this.readDb` (the replica, when one is
|
|
209
|
+
configured) — plus the CRUD set as protected methods.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// server/repositories/order.ts
|
|
213
|
+
import { BaseRepository } from '@spfn/core/db';
|
|
214
|
+
import { desc } from 'drizzle-orm';
|
|
215
|
+
import { orders } from '../entities/order';
|
|
216
|
+
|
|
217
|
+
export class OrderRepository extends BaseRepository
|
|
218
|
+
{
|
|
219
|
+
findRecentFor(userId: string)
|
|
220
|
+
{
|
|
221
|
+
return this._findMany(orders, { where: { userId }, orderBy: desc(orders.createdAt) });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
place(data: { userId: string; total: number })
|
|
225
|
+
{
|
|
226
|
+
return this._create(orders, data);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const orderRepo = new OrderRepository();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Handlers never import drizzle query builders; repositories do. `_findMany` reads through
|
|
234
|
+
`this.readDb`, `_create` writes through `this.db`, and both getters resolve to the active
|
|
235
|
+
transaction's connection when there is one — so the same method is correct inside a
|
|
236
|
+
transaction and outside it. When a helper cannot express a query, drop to
|
|
237
|
+
`this.readDb.select()…` inside the repository rather than in the handler. The full
|
|
238
|
+
protected CRUD set is in [src/db](./src/db/README.md).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Where do transactions and their side effects go?
|
|
243
|
+
|
|
244
|
+
`Transactional()` covers the route case — commit on return, rollback on throw. Two rules
|
|
245
|
+
decide the rest.
|
|
246
|
+
|
|
247
|
+
**Nothing takes a `tx` parameter.** The transaction travels in AsyncLocalStorage, so
|
|
248
|
+
`this.db` inside a repository already resolves to it. A service that accepts `tx` and
|
|
249
|
+
threads it downward re-implements propagation that already happened, and the first caller
|
|
250
|
+
that forgets to pass it writes outside the transaction. For a service, script or job with
|
|
251
|
+
no route around it, open one with `runInTransaction(fn, options?)`;
|
|
252
|
+
`runWithTransaction(tx, txId, fn)` is the lower-level primitive that binds an existing
|
|
253
|
+
Drizzle transaction into the context.
|
|
254
|
+
|
|
255
|
+
**Side effects go on the commit hooks, not inline.** An event emitted or a mail sent from
|
|
256
|
+
inside the transaction still went out when the transaction later rolls back.
|
|
257
|
+
|
|
258
|
+
| Hook | When it runs | What it is for |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `onBeforeCommit(fn)` | Inside the still-open transaction, just before commit — a throw aborts and rolls back | Last-moment invariant checks, and statements that must land in the same commit |
|
|
261
|
+
| `onAfterCommit(fn)` | After the root transaction commits, outside the transaction context; errors are logged, never thrown | Events, mail, cache invalidation — anything the outside world observes |
|
|
262
|
+
| `onAfterRollback(fn)` | After the root transaction rolls back, before the causing error propagates; errors are logged, never thrown | Undoing external work that cannot roll itself back, such as an object already uploaded |
|
|
263
|
+
|
|
264
|
+
All three import from `@spfn/core/db`, can be registered anywhere inside the transaction,
|
|
265
|
+
and bubble to the **root** transaction — a nested block's callbacks fire on the outermost
|
|
266
|
+
outcome, not on a savepoint's.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { runInTransaction, onAfterCommit } from '@spfn/core/db';
|
|
270
|
+
|
|
271
|
+
export async function placeOrder(input: { userId: string; total: number })
|
|
272
|
+
{
|
|
273
|
+
return runInTransaction(async () =>
|
|
274
|
+
{
|
|
275
|
+
const order = await orderRepo.place(input); // no tx argument, at any depth
|
|
276
|
+
onAfterCommit(() => orderPlacedEvent.emit({ orderId: order.id }));
|
|
277
|
+
|
|
278
|
+
return order;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## What else can defineServerConfig configure?
|
|
286
|
+
|
|
287
|
+
`.port()` and `.routes()` are the two every app calls. The rest of the builder is how an
|
|
288
|
+
app wires its infrastructure without touching the server's boot sequence. Every method
|
|
289
|
+
returns the builder; `.build()` ends the chain.
|
|
290
|
+
|
|
291
|
+
| Method | What it configures |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `.port(n)` / `.host(s)` | Where the server listens |
|
|
294
|
+
| `.routes(router)` | The `defineRouter` router to mount, with its own `.use()` and `.packages()` |
|
|
295
|
+
| `.jobs(router, config?)` | Background jobs — a `defineJobRouter`, plus pg-boss options |
|
|
296
|
+
| `.events(router, config?)` | SSE streaming — a `defineEventRouter`, served at `GET /events/stream` |
|
|
297
|
+
| `.websockets(router, config?)` | Bidirectional WebSockets — a `defineWSRouter`, served at `WS /ws` |
|
|
298
|
+
| `.workflows(router, config?)` | `@spfn/workflow` orchestration; the engine starts once the database is ready |
|
|
299
|
+
| `.lifecycle(hooks)` | Boot and shutdown hooks. Callable more than once; hooks run in registration order |
|
|
300
|
+
| `.migrations(opts)` | The migration boot gate — `{ allowPending: true }` lets a server start behind its migrations |
|
|
301
|
+
| `.database(opts)` | Connection and pool settings |
|
|
302
|
+
| `.infrastructure(opts)` | Which infrastructure is initialized at boot |
|
|
303
|
+
| `.healthCheck(opts)` | The health endpoint |
|
|
304
|
+
| `.serverTime(clock)` | The clock behind `GET /_core/time`; normally left at the default |
|
|
305
|
+
| `.middleware(opts)` | The built-in middleware — `ErrorHandler`, `RequestLogger` |
|
|
306
|
+
| `.use(handlers)` | Additional global Hono middleware |
|
|
307
|
+
| `.middlewares(named)` | Global middleware under names, so a route can `.skip([...])` it |
|
|
308
|
+
| `.cors(opts)` | CORS |
|
|
309
|
+
| `.rateLimit(opts)` | A global default limiter plus the named policies routes resolve against |
|
|
310
|
+
| `.proxyGuard(opts)` | Trusted-proxy signature and origin verification, resolved to a `clientType` |
|
|
311
|
+
| `.outboundFetch(opts)` | The SSRF policy `safeFetch` applies to outbound calls |
|
|
312
|
+
| `.timeout(opts)` / `.shutdown(opts)` | Request timeouts and graceful shutdown |
|
|
313
|
+
| `.debug(bool)` | Debug logging |
|
|
314
|
+
|
|
315
|
+
The options each one takes are in [src/server](./src/server/README.md).
|
|
316
|
+
|
|
193
317
|
---
|
|
194
318
|
|
|
195
319
|
## Which import path do I use for what?
|
|
196
320
|
|
|
197
321
|
There is **no root barrel**: `import … from '@spfn/core'` does not resolve. Every symbol
|
|
198
322
|
comes from a subpath, and the table below is the complete public surface — one row per
|
|
199
|
-
entry in `package.json` `exports
|
|
323
|
+
entry in `package.json` `exports`, with a single exclusion: `./client` is still listed in
|
|
324
|
+
`exports` but the build no longer emits it, so it has no row (see [Pitfalls](#pitfalls)).
|
|
325
|
+
Each module has its own README with the API detail.
|
|
200
326
|
|
|
201
327
|
| Import path | Purpose | Doc |
|
|
202
328
|
|-------------|---------|-----|
|
|
@@ -207,16 +333,17 @@ entry in `package.json` `exports`. Each module has its own README with the API d
|
|
|
207
333
|
| `@spfn/core/nextjs/server` | Server-only: `createRpcProxy({ routeMap })`, `registerInterceptors`. Uses `next/headers`. | [src/nextjs](./src/nextjs/README.md) |
|
|
208
334
|
| `@spfn/core/db` | PostgreSQL through Drizzle: CRUD helpers, `BaseRepository`, schema helpers, transactions, Postgres error mapping. One entry point for all of it. | [src/db](./src/db/README.md) |
|
|
209
335
|
| `@spfn/core/db` → manager | Connection lifecycle, pool, primary/replica, health check, reconnect (`initDatabase`, `getDatabase`). Re-exported from `@spfn/core/db`. | [src/db/manager](./src/db/manager/README.md) |
|
|
210
|
-
| `@spfn/core/db` → migrations | Which migrations each installed function package ships, and which the database has applied (`collectMigrationStatus`, `discoverFunctionMigrations`). What `spfn db status`, the boot gate and health all read. Re-exported from `@spfn/core/db`. | [src/db/migrations](
|
|
336
|
+
| `@spfn/core/db` → migrations | Which migrations each installed function package ships, and which the database has applied (`collectMigrationStatus`, `discoverFunctionMigrations`). What `spfn db status`, the boot gate and health all read. Re-exported from `@spfn/core/db`. | [src/db/migrations](https://github.com/fxylabs/spfn/blob/main/packages/core/src/db/migrations/index.ts) |
|
|
211
337
|
| `@spfn/core/db` → schema | Drizzle column helpers (`id`, `uuid`, `timestamps`, `foreignKey`, `enumText`, `typedJsonb`, `softDelete`, …). Re-exported from `@spfn/core/db`. | [src/db/schema](./src/db/schema/README.md) |
|
|
212
338
|
| `@spfn/core/db` → transaction | `Transactional()` middleware and `runInTransaction`; the transaction reaches every repository through AsyncLocalStorage. Re-exported from `@spfn/core/db`. | [src/db/transaction](./src/db/transaction/README.md) |
|
|
213
339
|
| `@spfn/core/middleware` | Built-in Hono middleware: `ErrorHandler`, `RequestLogger` and its masking helper. | [src/middleware](./src/middleware/README.md) |
|
|
214
340
|
| `@spfn/core/errors` | Serializable HTTP and database error classes, plus `ErrorRegistry` so an error survives the trip to the client as its own class. | [src/errors](./src/errors/README.md) |
|
|
215
341
|
| `@spfn/core/security` | `safeFetch` — a drop-in `fetch` hardened against SSRF, including DNS rebinding, by pinning the connection to a validated IP. | [src/security](./src/security/README.md) |
|
|
216
|
-
| `@spfn/core/authz` | Ownership guards. `requireOwner(resource, userId)` makes "load it, then check it belongs to the requester" one call, so a handler cannot forget it. | [src/authz/index.ts](
|
|
342
|
+
| `@spfn/core/authz` | Ownership guards. `requireOwner(resource, userId)` makes "load it, then check it belongs to the requester" one call, so a handler cannot forget it. | [src/authz/index.ts](https://github.com/fxylabs/spfn/blob/main/packages/core/src/authz/index.ts) |
|
|
217
343
|
| `@spfn/core/env` | Schema-based environment validation, isomorphic. | [src/env](./src/env/README.md) |
|
|
218
344
|
| `@spfn/core/env/loader` | The **server-only** `.env` file loader (uses `node:fs`). | [src/env](./src/env/README.md) |
|
|
219
345
|
| `@spfn/core/config` | `@spfn/core`'s own validated env config (`env`, `envSchema`, `registry`), built on `@spfn/core/env`. | [src/config](./src/config/README.md) |
|
|
346
|
+
| `@spfn/core/app-config` | Reads `spfn.config.js` — the one committed place that says which ports and host the app is served on (`loadAppConfig`, `resolvePorts`, `resolveHost`, `PORT_DEFAULTS`). Deliberately side-effect free, so the CLI can import it before an app's environment exists. | [src/app-config/index.ts](https://github.com/fxylabs/spfn/blob/main/packages/core/src/app-config/index.ts) |
|
|
220
347
|
| `@spfn/core/logger` | Structured singleton `logger` with child loggers and level masking. No dependencies. | [src/logger](./src/logger/README.md) |
|
|
221
348
|
| `@spfn/core/cache` | Valkey/Redis singleton over ioredis (`getCache`, `getCacheRead`). Degrades to disabled rather than throwing. | [src/cache](./src/cache/README.md) |
|
|
222
349
|
| `@spfn/core/job` | Background jobs on pg-boss: a fluent `job()` builder, cron, run-once, event-driven, `defineJobRouter`. | [src/job](./src/job/README.md) |
|
|
@@ -227,6 +354,7 @@ entry in `package.json` `exports`. Each module has its own README with the API d
|
|
|
227
354
|
| `@spfn/core/event/ws/client` | Browser WebSocket client. | [src/event](./src/event/README.md) |
|
|
228
355
|
| `@spfn/core/codegen` | The codegen orchestrator and the built-in generators: `@spfn/core:route-map` for the proxy's route map, `@spfn/core:contract` for the client contract. | [src/codegen](./src/codegen/README.md) |
|
|
229
356
|
| `@spfn/core/contract` | Route contracts for clients that ship separately: collect, snapshot, and the build gate that refuses a breaking change. | [src/contract](./src/contract/README.md) |
|
|
357
|
+
| `@spfn/core/ops` | The operations surface `spfn ops` drives: `opsRoute`, `createOpsRouter`, `defineOpsModule`, and the manifest the CLI discovers commands from. Structure only — the router is always authenticated, and token verification lives in `@spfn/auth`. | [How do I operate the app from the terminal?](#how-do-i-operate-the-app-from-the-terminal) |
|
|
230
358
|
|
|
231
359
|
`db/manager`, `db/schema` and `db/transaction` are **not** package subpaths of their own.
|
|
232
360
|
They are internal modules re-exported by `@spfn/core/db` — import their symbols from
|
package/dist/db/index.d.ts
CHANGED
|
@@ -397,27 +397,6 @@ declare function getDatabaseInfo(): {
|
|
|
397
397
|
providerKind?: string;
|
|
398
398
|
};
|
|
399
399
|
|
|
400
|
-
/**
|
|
401
|
-
* Reconnect Trigger — Query-error driven pool rebuild
|
|
402
|
-
*
|
|
403
|
-
* Complements the periodic health check with a fast-path: when application
|
|
404
|
-
* queries start failing with connection-level errors, we do not wait up to
|
|
405
|
-
* DB_HEALTH_CHECK_INTERVAL (default 60s) to notice. A sliding-window counter
|
|
406
|
-
* trips a force-reconnect as soon as the failure rate crosses a threshold.
|
|
407
|
-
*
|
|
408
|
-
* Why this exists:
|
|
409
|
-
* - postgres.js transparently drops dead sockets and opens new ones on the
|
|
410
|
-
* next query. A single `SELECT 1` on the periodic interval can therefore
|
|
411
|
-
* false-pass while user-facing queries keep hitting the remaining dead
|
|
412
|
-
* sockets in the pool.
|
|
413
|
-
* - This module observes real query errors and, when it sees a burst of
|
|
414
|
-
* connection-level failures, calls triggerForceReconnect() which performs
|
|
415
|
-
* the same atomic-swap rebuild as the health check.
|
|
416
|
-
*
|
|
417
|
-
* Configuration (env vars, hardcoded defaults):
|
|
418
|
-
* - DB_RECONNECT_ERROR_THRESHOLD (default 3): errors needed in window
|
|
419
|
-
* - DB_RECONNECT_ERROR_WINDOW_MS (default 10000): sliding window size
|
|
420
|
-
*/
|
|
421
400
|
/**
|
|
422
401
|
* Determine whether an error looks like a pool/connection failure
|
|
423
402
|
*
|
|
@@ -1139,6 +1118,23 @@ type TransactionDB<TDatabase extends DrizzleDatabase = DefaultDatabase> = Databa
|
|
|
1139
1118
|
* afterCommit callback type
|
|
1140
1119
|
*/
|
|
1141
1120
|
type AfterCommitCallback = () => void | Promise<void>;
|
|
1121
|
+
/**
|
|
1122
|
+
* beforeCommit callback type
|
|
1123
|
+
*/
|
|
1124
|
+
type BeforeCommitCallback = () => void | Promise<void>;
|
|
1125
|
+
/**
|
|
1126
|
+
* afterRollback callback type
|
|
1127
|
+
*/
|
|
1128
|
+
type AfterRollbackCallback = () => void | Promise<void>;
|
|
1129
|
+
/**
|
|
1130
|
+
* Serializes the savepoint frames opened directly off one transaction context
|
|
1131
|
+
*
|
|
1132
|
+
* @see createNestedFrameGate
|
|
1133
|
+
*/
|
|
1134
|
+
type NestedFrameGate = {
|
|
1135
|
+
/** Run `frame` once every frame queued before it on this context has finished */
|
|
1136
|
+
run<T>(frame: () => Promise<T>): Promise<T>;
|
|
1137
|
+
};
|
|
1142
1138
|
/**
|
|
1143
1139
|
* Transaction context stored in AsyncLocalStorage
|
|
1144
1140
|
*/
|
|
@@ -1148,8 +1144,14 @@ type TransactionContext<TDatabase extends DrizzleDatabase = DrizzleDatabase> = {
|
|
|
1148
1144
|
/** Unique transaction ID for logging and tracing */
|
|
1149
1145
|
txId: string;
|
|
1150
1146
|
level: number;
|
|
1147
|
+
/** Callbacks to execute before the root transaction commits (still inside it) */
|
|
1148
|
+
beforeCommitCallbacks: BeforeCommitCallback[];
|
|
1151
1149
|
/** Callbacks to execute after root transaction commits */
|
|
1152
1150
|
afterCommitCallbacks: AfterCommitCallback[];
|
|
1151
|
+
/** Callbacks to execute after the root transaction rolled back */
|
|
1152
|
+
afterRollbackCallbacks: AfterRollbackCallback[];
|
|
1153
|
+
/** Serializes the savepoint frames opened directly off THIS context */
|
|
1154
|
+
nestedFrames: NestedFrameGate;
|
|
1153
1155
|
};
|
|
1154
1156
|
/**
|
|
1155
1157
|
* Get current transaction object and metadata from AsyncLocalStorage
|
|
@@ -1187,7 +1189,7 @@ callback: () => Promise<T>): Promise<T>;
|
|
|
1187
1189
|
*
|
|
1188
1190
|
* @example
|
|
1189
1191
|
* ```typescript
|
|
1190
|
-
* import { onAfterCommit } from '@spfn/core/db
|
|
1192
|
+
* import { onAfterCommit } from '@spfn/core/db';
|
|
1191
1193
|
*
|
|
1192
1194
|
* async function submit(spaceId: string, chatId: string)
|
|
1193
1195
|
* {
|
|
@@ -1201,6 +1203,77 @@ callback: () => Promise<T>): Promise<T>;
|
|
|
1201
1203
|
* ```
|
|
1202
1204
|
*/
|
|
1203
1205
|
declare function onAfterCommit(callback: AfterCommitCallback): void;
|
|
1206
|
+
/**
|
|
1207
|
+
* Register a callback to run just before the current transaction commits
|
|
1208
|
+
*
|
|
1209
|
+
* - Inside a transaction: queued and executed after the root callback resolves,
|
|
1210
|
+
* while the transaction is still open — the callback MAY run statements and
|
|
1211
|
+
* they are part of the same commit
|
|
1212
|
+
* - Outside a transaction: executed immediately with a WARNING (same rationale as
|
|
1213
|
+
* onAfterCommit — there is nothing left to commit), and the abort semantics
|
|
1214
|
+
* below do not apply: there is no transaction for a throw to roll back
|
|
1215
|
+
* - Nested transactions: callbacks bubble up to root transaction
|
|
1216
|
+
* - Callbacks run in registration order, inside the transaction context
|
|
1217
|
+
* (getTransaction() returns the live tx)
|
|
1218
|
+
* - The queue is snapshot before the pass, so a callback registered BY a
|
|
1219
|
+
* beforeCommit callback does not run for this commit — it would otherwise
|
|
1220
|
+
* grow the queue mid-iteration and loop forever inside the open transaction
|
|
1221
|
+
* - A throw ABORTS: later callbacks are skipped, the transaction rolls back,
|
|
1222
|
+
* the error propagates to the caller, and afterRollback callbacks fire
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```typescript
|
|
1226
|
+
* import { runInTransaction, onBeforeCommit } from '@spfn/core/db';
|
|
1227
|
+
*
|
|
1228
|
+
* async function transfer(fromId: string, toId: string, amount: number)
|
|
1229
|
+
* {
|
|
1230
|
+
* // The transaction is what gives the check teeth — registered outside one,
|
|
1231
|
+
* // the callback runs immediately and its throw aborts nothing.
|
|
1232
|
+
* await runInTransaction(async () =>
|
|
1233
|
+
* {
|
|
1234
|
+
* await accountRepo.debit(fromId, amount);
|
|
1235
|
+
* await accountRepo.credit(toId, amount);
|
|
1236
|
+
*
|
|
1237
|
+
* // Last-moment invariant check: a throw here rolls the whole transfer back.
|
|
1238
|
+
* onBeforeCommit(() => assertNoNegativeBalance(fromId));
|
|
1239
|
+
* });
|
|
1240
|
+
* }
|
|
1241
|
+
* ```
|
|
1242
|
+
*/
|
|
1243
|
+
declare function onBeforeCommit(callback: BeforeCommitCallback): void;
|
|
1244
|
+
/**
|
|
1245
|
+
* Register a callback to run after the current transaction rolled back
|
|
1246
|
+
*
|
|
1247
|
+
* - Inside a transaction: queued and executed after the ROOT transaction rolled
|
|
1248
|
+
* back, before the causing error leaves runInTransaction / the middleware
|
|
1249
|
+
* - Outside a transaction: no-op with a warning — there is no rollback to wait for
|
|
1250
|
+
* - Nested transactions: callbacks bubble up to root transaction. A nested
|
|
1251
|
+
* rollback that the root survives does NOT fire them; these hooks are about
|
|
1252
|
+
* the root transaction's fate, not a savepoint's
|
|
1253
|
+
* - Callbacks run outside the transaction context (new connection for DB access)
|
|
1254
|
+
* - Errors are logged but never thrown: the original error keeps propagating
|
|
1255
|
+
* unchanged, never replaced by a callback failure
|
|
1256
|
+
*
|
|
1257
|
+
* @example
|
|
1258
|
+
* ```typescript
|
|
1259
|
+
* import { runInTransaction, onAfterRollback } from '@spfn/core/db';
|
|
1260
|
+
*
|
|
1261
|
+
* async function importAvatar(userId: string, file: Blob)
|
|
1262
|
+
* {
|
|
1263
|
+
* // Upload first: external I/O never belongs inside the transaction.
|
|
1264
|
+
* const key = await objectStore.put(file);
|
|
1265
|
+
*
|
|
1266
|
+
* await runInTransaction(async () =>
|
|
1267
|
+
* {
|
|
1268
|
+
* await userRepo.updateAvatar(userId, key);
|
|
1269
|
+
*
|
|
1270
|
+
* // The upload cannot roll back on its own — undo it if the write never lands.
|
|
1271
|
+
* onAfterRollback(() => objectStore.delete(key));
|
|
1272
|
+
* });
|
|
1273
|
+
* }
|
|
1274
|
+
* ```
|
|
1275
|
+
*/
|
|
1276
|
+
declare function onAfterRollback(callback: AfterRollbackCallback): void;
|
|
1204
1277
|
|
|
1205
1278
|
/**
|
|
1206
1279
|
* Transaction middleware options
|
|
@@ -1245,6 +1318,21 @@ interface TransactionalOptions {
|
|
|
1245
1318
|
* @default 30000 (30s) or TRANSACTION_IDLE_TIMEOUT environment variable
|
|
1246
1319
|
*/
|
|
1247
1320
|
idleTimeout?: number;
|
|
1321
|
+
/**
|
|
1322
|
+
* Run in an independent transaction instead of joining an ambient one.
|
|
1323
|
+
*
|
|
1324
|
+
* Only bites when the middleware itself runs nested — a sub-app mounted
|
|
1325
|
+
* under a route that already applied `Transactional()`, or a handler invoked
|
|
1326
|
+
* from inside `runInTransaction`. By default that inner run takes a SAVEPOINT
|
|
1327
|
+
* on the outer transaction; `requiresNew: true` gives it a real `BEGIN` on a
|
|
1328
|
+
* second pooled connection, with its own timeouts and its own hook queues.
|
|
1329
|
+
*
|
|
1330
|
+
* See `RunInTransactionOptions.requiresNew` for the pool and self-deadlock
|
|
1331
|
+
* costs — they apply here unchanged.
|
|
1332
|
+
*
|
|
1333
|
+
* @default false
|
|
1334
|
+
*/
|
|
1335
|
+
requiresNew?: boolean;
|
|
1248
1336
|
}
|
|
1249
1337
|
/**
|
|
1250
1338
|
* Transaction middleware for Hono routes
|
|
@@ -1282,6 +1370,9 @@ interface TransactionalOptions {
|
|
|
1282
1370
|
* - Success: Auto-commit
|
|
1283
1371
|
* - Error: Auto-rollback
|
|
1284
1372
|
* - Detects context.error to trigger rollback
|
|
1373
|
+
* - Hooks: this delegates to runInTransaction, so onBeforeCommit, onAfterCommit
|
|
1374
|
+
* and onAfterRollback behave exactly as they do there. afterRollback callbacks
|
|
1375
|
+
* have already run by the time the error reaches the conversion below.
|
|
1285
1376
|
*
|
|
1286
1377
|
* 📊 Transaction logging:
|
|
1287
1378
|
* - Auto-logs transaction start/commit/rollback
|
|
@@ -1317,8 +1408,12 @@ interface RunInTransactionOptions {
|
|
|
1317
1408
|
* - `timeout: undefined` - Uses default (30s or TRANSACTION_TIMEOUT env var)
|
|
1318
1409
|
* - `timeout: N` - Sets timeout to N milliseconds (1 to 2147483647)
|
|
1319
1410
|
*
|
|
1320
|
-
* Note: Timeout is only applied to root transactions.
|
|
1321
|
-
*
|
|
1411
|
+
* Note: Timeout is only applied to root transactions. A nested call takes a
|
|
1412
|
+
* SAVEPOINT on the outer transaction's connection, where the outer
|
|
1413
|
+
* transaction's `SET LOCAL statement_timeout` is already in force — so the
|
|
1414
|
+
* nested call genuinely inherits it, and its own `timeout` is ignored (a
|
|
1415
|
+
* warning is logged when the caller passed one explicitly). A
|
|
1416
|
+
* `requiresNew: true` call is a root and gets its own.
|
|
1322
1417
|
*
|
|
1323
1418
|
* @default 30000 (30 seconds) or TRANSACTION_TIMEOUT environment variable
|
|
1324
1419
|
*
|
|
@@ -1355,6 +1450,48 @@ interface RunInTransactionOptions {
|
|
|
1355
1450
|
* @default 'transaction'
|
|
1356
1451
|
*/
|
|
1357
1452
|
context?: string;
|
|
1453
|
+
/**
|
|
1454
|
+
* Run in an independent transaction instead of joining an ambient one.
|
|
1455
|
+
*
|
|
1456
|
+
* By default a call made while another transaction is open takes a SAVEPOINT
|
|
1457
|
+
* on that transaction's connection: its writes commit or roll back with the
|
|
1458
|
+
* outer transaction. `requiresNew: true` opens a real `BEGIN` on a SECOND
|
|
1459
|
+
* pooled connection instead, so the work commits on its own and survives an
|
|
1460
|
+
* outer rollback — an audit trail or a failed-attempt record, for example.
|
|
1461
|
+
*
|
|
1462
|
+
* Being a root transaction, it gets its own `statement_timeout`,
|
|
1463
|
+
* `idle_in_transaction_session_timeout`, and its OWN hook queues:
|
|
1464
|
+
* `onBeforeCommit` / `onAfterCommit` / `onAfterRollback` registered inside it
|
|
1465
|
+
* fire on ITS outcome, not the outer transaction's.
|
|
1466
|
+
*
|
|
1467
|
+
* Two costs, both consequences of the second connection:
|
|
1468
|
+
* - It holds a second connection for its whole duration, so it counts twice
|
|
1469
|
+
* against the pool. Keep it short and don't fan it out.
|
|
1470
|
+
* - It cannot see the outer transaction's uncommitted writes, and it BLOCKS
|
|
1471
|
+
* on any row the outer transaction has locked. Since the outer transaction
|
|
1472
|
+
* is waiting for this call to return, that block is a self-deadlock that
|
|
1473
|
+
* only `statement_timeout` breaks. Never touch rows the outer transaction
|
|
1474
|
+
* wrote.
|
|
1475
|
+
*
|
|
1476
|
+
* @default false
|
|
1477
|
+
*
|
|
1478
|
+
* @example
|
|
1479
|
+
* ```typescript
|
|
1480
|
+
* await runInTransaction(async () =>
|
|
1481
|
+
* {
|
|
1482
|
+
* await orderRepo.create(order);
|
|
1483
|
+
*
|
|
1484
|
+
* // Lands even if the order below rolls the outer transaction back.
|
|
1485
|
+
* await runInTransaction(
|
|
1486
|
+
* () => auditRepo.record('order.attempted', order.id),
|
|
1487
|
+
* { requiresNew: true },
|
|
1488
|
+
* );
|
|
1489
|
+
*
|
|
1490
|
+
* await inventoryRepo.reserve(order.items); // may throw
|
|
1491
|
+
* });
|
|
1492
|
+
* ```
|
|
1493
|
+
*/
|
|
1494
|
+
requiresNew?: boolean;
|
|
1358
1495
|
}
|
|
1359
1496
|
/**
|
|
1360
1497
|
* Run a callback function within a database transaction
|
|
@@ -1366,6 +1503,15 @@ interface RunInTransactionOptions {
|
|
|
1366
1503
|
* - Warns about slow transactions
|
|
1367
1504
|
* - Enforces timeout if configured
|
|
1368
1505
|
*
|
|
1506
|
+
* Called with a transaction already open on the async call chain, it takes a
|
|
1507
|
+
* SAVEPOINT on that transaction rather than opening an independent one: the work
|
|
1508
|
+
* runs on the same connection, sees the outer transaction's uncommitted writes,
|
|
1509
|
+
* and commits with it. A throw that the caller catches unwinds to the SAVEPOINT
|
|
1510
|
+
* and leaves the outer transaction healthy; a throw that propagates rolls the
|
|
1511
|
+
* whole thing back. Nested calls off one transaction are serialized — see
|
|
1512
|
+
* `openTransaction` — so `Promise.all` over them runs them one at a time. Pass
|
|
1513
|
+
* `requiresNew: true` for an independent transaction on its own connection.
|
|
1514
|
+
*
|
|
1369
1515
|
* Errors are propagated to the caller without modification.
|
|
1370
1516
|
* Caller is responsible for error handling and conversion.
|
|
1371
1517
|
*
|
|
@@ -1680,11 +1826,11 @@ declare function count<T extends PgTable>(table: T, where?: WhereObject<InferSel
|
|
|
1680
1826
|
*
|
|
1681
1827
|
* @example With Transactions
|
|
1682
1828
|
* ```typescript
|
|
1683
|
-
* import {
|
|
1829
|
+
* import { runInTransaction } from '@spfn/core/db';
|
|
1684
1830
|
*
|
|
1685
1831
|
* const userRepo = new UserRepository();
|
|
1686
1832
|
*
|
|
1687
|
-
* await
|
|
1833
|
+
* await runInTransaction(async () => {
|
|
1688
1834
|
* // Both db and readDb automatically use the transaction context
|
|
1689
1835
|
* const user = await userRepo.create({ name: 'John' });
|
|
1690
1836
|
* await userRepo.findById(user.id); // Uses same transaction
|
|
@@ -2006,4 +2152,4 @@ declare abstract class BaseRepository<TRelations extends AnyRelations = EmptyRel
|
|
|
2006
2152
|
protected _count<T extends PgTable>(table: T, where?: Record<string, any> | SQL | undefined): Promise<number>;
|
|
2007
2153
|
}
|
|
2008
2154
|
|
|
2009
|
-
export { type AfterCommitCallback, BaseRepository, type DatabaseClients, type DatabaseInitOptions, type DatabaseOptions, type DatabaseProvider, type DatabaseTransaction, type DefaultDatabase, type DrizzleConfigOptions, type DrizzleDatabase, type FunctionMigrationEntry, type FunctionMigrationInfo, type MigrationStatus, type MigrationStatusDb, type MigrationTargetStatus, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, type PoolConfig, RUN_MIGRATIONS_HINT, type RepositoryDatabase, RepositoryError, type RetryConfig, type RunInTransactionOptions, type TransactionContext, type TransactionDB, Transactional, type TransactionalOptions, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
2155
|
+
export { type AfterCommitCallback, type AfterRollbackCallback, BaseRepository, type BeforeCommitCallback, type DatabaseClients, type DatabaseInitOptions, type DatabaseOptions, type DatabaseProvider, type DatabaseTransaction, type DefaultDatabase, type DrizzleConfigOptions, type DrizzleDatabase, type FunctionMigrationEntry, type FunctionMigrationInfo, type MigrationStatus, type MigrationStatusDb, type MigrationTargetStatus, type NestedFrameGate, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, type PoolConfig, RUN_MIGRATIONS_HINT, type RepositoryDatabase, RepositoryError, type RetryConfig, type RunInTransactionOptions, type TransactionContext, type TransactionDB, Transactional, type TransactionalOptions, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, onAfterRollback, onBeforeCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|