@palbase/backend 23.0.0 → 23.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/dist/bin/palbase-backend.cjs +313 -24
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +5 -5
- package/dist/{chunk-Y5HXVUMP.js → chunk-HQRJDARQ.js} +2 -2
- package/dist/{chunk-REZU6UKT.js → chunk-M5MCBWJI.js} +296 -25
- package/dist/chunk-M5MCBWJI.js.map +1 -0
- package/dist/{chunk-W5ODXPY3.js → chunk-NS5V43YQ.js} +14 -1
- package/dist/chunk-NS5V43YQ.js.map +1 -0
- package/dist/{chunk-FSGSB42K.js → chunk-OHALWEOG.js} +9 -4
- package/dist/chunk-OHALWEOG.js.map +1 -0
- package/dist/{chunk-HAF67F2H.js → chunk-PY7YJDCT.js} +57 -3
- package/dist/chunk-PY7YJDCT.js.map +1 -0
- package/dist/{chunk-ZC6Q2BRD.js → chunk-R3KN6RHD.js} +2 -2
- package/dist/{chunk-OMRTHM4X.js → chunk-RCLNBJCM.js} +1 -1
- package/dist/chunk-RCLNBJCM.js.map +1 -0
- package/dist/db/index.cjs +68 -1
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +2 -2
- package/dist/{endpoint-BavvbW4P.d.ts → endpoint-CVWXh6oG.d.ts} +70 -8
- package/dist/{endpoint-i8TTCohk.d.cts → endpoint-c9h5jriX.d.cts} +70 -8
- package/dist/engine/index.cjs +313 -24
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +4 -4
- package/dist/engine/index.d.ts +4 -4
- package/dist/engine/index.js +4 -4
- package/dist/{index-B7YBEG5w.d.ts → index-BZrJXnVh.d.ts} +49 -4
- package/dist/{index-Bmvx1EvJ.d.cts → index-By8Dle5U.d.cts} +86 -17
- package/dist/{index-B3jmmItD.d.ts → index-CwAJ7HEe.d.ts} +86 -17
- package/dist/{index-E7OscPJT.d.cts → index-CxeQSfJP.d.cts} +49 -4
- package/dist/index.cjs +104 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +32 -6
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +29 -4
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +5 -2
- package/dist/openapi/index.d.ts +5 -2
- package/dist/openapi/index.js +32 -7
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-DY3d9l1k.d.ts → registry-B3niOVYp.d.ts} +101 -7
- package/dist/{registry-C3H2uPeZ.d.cts → registry-CqPK2Qby.d.cts} +101 -7
- package/dist/test/index.cjs +482 -9
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +35 -3
- package/dist/test/index.d.ts +35 -3
- package/dist/test/index.js +480 -8
- package/dist/test/index.js.map +1 -1
- package/package.json +4 -3
- package/stager/return_types.js +23 -0
- package/dist/chunk-FSGSB42K.js.map +0 -1
- package/dist/chunk-HAF67F2H.js.map +0 -1
- package/dist/chunk-OMRTHM4X.js.map +0 -1
- package/dist/chunk-REZU6UKT.js.map +0 -1
- package/dist/chunk-W5ODXPY3.js.map +0 -1
- /package/dist/{chunk-Y5HXVUMP.js.map → chunk-HQRJDARQ.js.map} +0 -0
- /package/dist/{chunk-ZC6Q2BRD.js.map → chunk-R3KN6RHD.js.map} +0 -0
package/dist/engine/index.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { A as App, h as AuthVerifier, B as BootRefused, i as CreateAppOptions, E as EgressPolicy, j as EngineConfig, M as ModuleClients, k as RateLimiter, l as RequestDatabase, m as RouteEntry, n as RuntimeHooks, o as ScrubResult, p as SqlDriver, q as SqlTx, r as buildRouteTable, s as createApp, t as createLazyTransaction, u as createOps, v as createRequestDatabase, w as effectiveAuth, x as hostAllowed, y as installEgressFence, z as loadConfig, G as makeMemoryCache, H as matchRoute, I as quoteIdent, J as scrubSecrets, K as withTables } from '../index-
|
|
2
|
-
import '../endpoint-
|
|
1
|
+
export { A as App, h as AuthVerifier, B as BootRefused, i as CreateAppOptions, E as EgressPolicy, j as EngineConfig, M as ModuleClients, k as RateLimiter, l as RequestDatabase, m as RouteEntry, n as RuntimeHooks, o as ScrubResult, p as SqlDriver, q as SqlTx, r as buildRouteTable, s as createApp, t as createLazyTransaction, u as createOps, v as createRequestDatabase, w as effectiveAuth, x as hostAllowed, y as installEgressFence, z as loadConfig, G as makeMemoryCache, H as matchRoute, I as quoteIdent, J as scrubSecrets, K as withTables } from '../index-CxeQSfJP.cjs';
|
|
2
|
+
import '../endpoint-c9h5jriX.cjs';
|
|
3
3
|
import '../stack.cjs';
|
|
4
4
|
import 'node:async_hooks';
|
|
5
|
-
import '../index-
|
|
5
|
+
import '../index-By8Dle5U.cjs';
|
|
6
6
|
import '../db/env.cjs';
|
|
7
|
-
import '../registry-
|
|
7
|
+
import '../registry-CqPK2Qby.cjs';
|
|
8
8
|
import 'zod';
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { A as App, h as AuthVerifier, B as BootRefused, i as CreateAppOptions, E as EgressPolicy, j as EngineConfig, M as ModuleClients, k as RateLimiter, l as RequestDatabase, m as RouteEntry, n as RuntimeHooks, o as ScrubResult, p as SqlDriver, q as SqlTx, r as buildRouteTable, s as createApp, t as createLazyTransaction, u as createOps, v as createRequestDatabase, w as effectiveAuth, x as hostAllowed, y as installEgressFence, z as loadConfig, G as makeMemoryCache, H as matchRoute, I as quoteIdent, J as scrubSecrets, K as withTables } from '../index-
|
|
2
|
-
import '../endpoint-
|
|
1
|
+
export { A as App, h as AuthVerifier, B as BootRefused, i as CreateAppOptions, E as EgressPolicy, j as EngineConfig, M as ModuleClients, k as RateLimiter, l as RequestDatabase, m as RouteEntry, n as RuntimeHooks, o as ScrubResult, p as SqlDriver, q as SqlTx, r as buildRouteTable, s as createApp, t as createLazyTransaction, u as createOps, v as createRequestDatabase, w as effectiveAuth, x as hostAllowed, y as installEgressFence, z as loadConfig, G as makeMemoryCache, H as matchRoute, I as quoteIdent, J as scrubSecrets, K as withTables } from '../index-BZrJXnVh.js';
|
|
2
|
+
import '../endpoint-CVWXh6oG.js';
|
|
3
3
|
import '../stack.js';
|
|
4
4
|
import 'node:async_hooks';
|
|
5
|
-
import '../index-
|
|
5
|
+
import '../index-CwAJ7HEe.js';
|
|
6
6
|
import '../db/env.js';
|
|
7
|
-
import '../registry-
|
|
7
|
+
import '../registry-B3niOVYp.js';
|
|
8
8
|
import 'zod';
|
package/dist/engine/index.js
CHANGED
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
quoteIdent,
|
|
17
17
|
scrubSecrets,
|
|
18
18
|
withTables
|
|
19
|
-
} from "../chunk-
|
|
20
|
-
import "../chunk-
|
|
21
|
-
import "../chunk-
|
|
22
|
-
import "../chunk-
|
|
19
|
+
} from "../chunk-M5MCBWJI.js";
|
|
20
|
+
import "../chunk-OHALWEOG.js";
|
|
21
|
+
import "../chunk-NS5V43YQ.js";
|
|
22
|
+
import "../chunk-RCLNBJCM.js";
|
|
23
23
|
import "../chunk-7D4SUZUM.js";
|
|
24
24
|
export {
|
|
25
25
|
AuthVerifier,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Buckets, BucketTypes } from './stack.js';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
-
import { C as CacheClient, P as PalbaseDocsClient, a as PalbaseFlagsClient, L as Logger, b as PalbaseNotificationsClient, c as PalbaseRealtimeClient, D as DBClient, S as SecretsService, d as PalbaseStorageClient, e as PalbaseBucketClient, T as TxPlanBody,
|
|
4
|
-
import { E as EnvTypedDatabase } from './index-
|
|
5
|
-
import { R as RouteMeta } from './registry-
|
|
3
|
+
import { C as CacheClient, P as PalbaseDocsClient, a as PalbaseFlagsClient, L as Logger, b as PalbaseNotificationsClient, c as PalbaseRealtimeClient, D as DBClient, S as SecretsService, d as PalbaseStorageClient, e as PalbaseBucketClient, f as DBOps, T as TxPlanBody, g as TxPlanResponse } from './endpoint-CVWXh6oG.js';
|
|
4
|
+
import { E as EnvTypedDatabase } from './index-CwAJ7HEe.js';
|
|
5
|
+
import { R as RouteMeta } from './registry-B3niOVYp.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* runtime.ts — request-scoped service singletons.
|
|
@@ -361,14 +361,41 @@ declare function createLazyTransaction(sql: SqlDriver, role: string, claimsJson:
|
|
|
361
361
|
type LazyTransaction = ReturnType<typeof createLazyTransaction>;
|
|
362
362
|
/** Either a live driver transaction or the lazy holder above. */
|
|
363
363
|
type TxLike = SqlTx | LazyTransaction;
|
|
364
|
+
/** What `findMany` accepts beside its filter: an ordering and a row ceiling.
|
|
365
|
+
* Both used to require dropping to raw SQL, and the docs said so — which is how
|
|
366
|
+
* a tenant's controllers filled up with hand-written SELECTs. */
|
|
367
|
+
interface FindManyOptions {
|
|
368
|
+
orderBy?: {
|
|
369
|
+
column: string;
|
|
370
|
+
direction?: "asc" | "desc";
|
|
371
|
+
};
|
|
372
|
+
limit?: number;
|
|
373
|
+
}
|
|
364
374
|
/** The six string-keyed operations, plus an interactive `transaction`. */
|
|
365
375
|
declare function createOps(tx: TxLike): {
|
|
366
376
|
query(sql: string, params?: unknown[]): Promise<Row[]>;
|
|
367
377
|
insert(table: string, data: Row): Promise<Row>;
|
|
378
|
+
/**
|
|
379
|
+
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
380
|
+
*
|
|
381
|
+
* WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
|
|
382
|
+
* unique violation, update instead" does not work here: a request runs in ONE
|
|
383
|
+
* Postgres transaction, so the failed insert aborts it and every later
|
|
384
|
+
* statement answers `current transaction is aborted`. A tenant measured that
|
|
385
|
+
* as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
|
|
386
|
+
* trigger create the row instead — a workaround that needs a new trigger for
|
|
387
|
+
* every table with a unique row.
|
|
388
|
+
*
|
|
389
|
+
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
390
|
+
* so writing them back is at best a no-op and at worst a surprise.
|
|
391
|
+
*/
|
|
392
|
+
upsert(table: string, data: Row, opts: {
|
|
393
|
+
onConflict: readonly string[];
|
|
394
|
+
}): Promise<Row>;
|
|
368
395
|
update(table: string, id: string, data: Row): Promise<Row | null>;
|
|
369
396
|
delete(table: string, id: string): Promise<void>;
|
|
370
397
|
findById(table: string, id: string): Promise<Row | null>;
|
|
371
|
-
findMany(table: string, query?: Row): Promise<Row[]>;
|
|
398
|
+
findMany(table: string, query?: Row, opts?: FindManyOptions): Promise<Row[]>;
|
|
372
399
|
/**
|
|
373
400
|
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
374
401
|
* (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
|
|
@@ -476,6 +503,24 @@ declare function createOps(tx: TxLike): {
|
|
|
476
503
|
supersede(table: string, id: string, row: Row): Promise<Row>;
|
|
477
504
|
/** A real SAVEPOINT inside the request's transaction. */
|
|
478
505
|
transaction<T>(cb: (t: unknown) => Promise<T>): Promise<T>;
|
|
506
|
+
/**
|
|
507
|
+
* Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
|
|
508
|
+
* rolls back only what that handle wrote and the request can keep writing.
|
|
509
|
+
*
|
|
510
|
+
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
511
|
+
* ... Database.insert(...) ... })`, with no parameter — would have to point
|
|
512
|
+
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
513
|
+
* concurrent with itself: `Promise.all([Database.insert(a),
|
|
514
|
+
* Database.attempt(...)])` would put `a` inside the savepoint and roll it
|
|
515
|
+
* back with it. Silent data loss, and the same interleaving this file already
|
|
516
|
+
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
517
|
+
* you can see in the code that crosses it.
|
|
518
|
+
*
|
|
519
|
+
* Postgres, not us: the savepoint is released on success and rolled back on
|
|
520
|
+
* failure by the driver, so an aborted statement inside `fn` does not poison
|
|
521
|
+
* the surrounding transaction.
|
|
522
|
+
*/
|
|
523
|
+
attempt<T>(fn: (tx: DBOps) => Promise<T>): Promise<T>;
|
|
479
524
|
/**
|
|
480
525
|
* Execute a whole transaction plan — what `Database.transaction(fn)` builds.
|
|
481
526
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tables, TableTypes } from './db/env.cjs';
|
|
2
|
-
import { D as DBClient, b8 as TxPlanHandle, bh as TxTable, M as Materialized } from './endpoint-
|
|
2
|
+
import { D as DBClient, b8 as TxPlanHandle, bh as TxTable, M as Materialized } from './endpoint-c9h5jriX.cjs';
|
|
3
3
|
|
|
4
4
|
/** On delete action for foreign key references. */
|
|
5
5
|
type OnDeleteAction = 'cascade' | 'set null' | 'restrict' | 'no action';
|
|
@@ -36,6 +36,17 @@ interface ColumnDef {
|
|
|
36
36
|
enumName?: string;
|
|
37
37
|
enumValues?: string[];
|
|
38
38
|
unique?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The value is written by the DATABASE — a trigger, a rule, an identity — not by
|
|
41
|
+
* the author and not by a DEFAULT this schema declares. It makes the column
|
|
42
|
+
* optional on INSERT without putting a DEFAULT in the DDL.
|
|
43
|
+
*
|
|
44
|
+
* Before this existed the only way to keep a trigger-filled column off the
|
|
45
|
+
* INSERT type was to give it a fake `default()`: a value the schema claimed to
|
|
46
|
+
* write and the trigger immediately overwrote. That made the schema lie about
|
|
47
|
+
* its own data.
|
|
48
|
+
*/
|
|
49
|
+
dbAssigned?: boolean;
|
|
39
50
|
/** vector(n): the declared dimension count — part of the TYPE (typmod), read
|
|
40
51
|
* by the wire serializer and the deploy's auto-index (FR-001). */
|
|
41
52
|
dimensions?: number;
|
|
@@ -80,6 +91,18 @@ declare class ColumnBuilder<K extends ColumnType = ColumnType, N extends boolean
|
|
|
80
91
|
defaultRandom(): ColumnBuilder<K, N, true, E, P>;
|
|
81
92
|
/** Timestamp: default to now(). */
|
|
82
93
|
defaultNow(): ColumnBuilder<K, N, true, E, P>;
|
|
94
|
+
/**
|
|
95
|
+
* The DATABASE assigns this column's value — a trigger, a rule, an identity.
|
|
96
|
+
*
|
|
97
|
+
* The column becomes optional on INSERT (the author has nothing to send) while
|
|
98
|
+
* the DDL stays free of a DEFAULT this schema would not honour. It is NOT
|
|
99
|
+
* `default()`: that declares a value the schema promises to write.
|
|
100
|
+
*
|
|
101
|
+
* Naming: deliberately not `generated()`. Postgres has GENERATED columns and
|
|
102
|
+
* they are a different thing; borrowing the word would send a reader — or a
|
|
103
|
+
* model writing a schema — to the wrong feature.
|
|
104
|
+
*/
|
|
105
|
+
dbAssigned(): ColumnBuilder<K, N, true, E, P>;
|
|
83
106
|
/** Add a foreign key reference. */
|
|
84
107
|
/**
|
|
85
108
|
* Declares that this column used to be called `previous`.
|
|
@@ -247,22 +270,6 @@ interface PolicyDef {
|
|
|
247
270
|
withCheck: string | null;
|
|
248
271
|
permissive: boolean;
|
|
249
272
|
}
|
|
250
|
-
/**
|
|
251
|
-
* Fluent RLS policy builder.
|
|
252
|
-
*
|
|
253
|
-
* Defaults (documented, applied at construction):
|
|
254
|
-
* - `command`: `"all"` — applies to every SQL command unless `.for(...)` narrows it.
|
|
255
|
-
* - `roles`: `["authenticated"]` — the common case is "rule applies to signed-in
|
|
256
|
-
* users". Call `.to(...)` to override; pass `.to()` with no roles (or never
|
|
257
|
-
* call it after a reset) to target PUBLIC.
|
|
258
|
-
* - `using` / `withCheck`: `null` — no row filter / write check until set.
|
|
259
|
-
* - `permissive`: `true` — `AS PERMISSIVE` (policies OR together).
|
|
260
|
-
*
|
|
261
|
-
* Each method mutates `_def` in place and returns `this`, so the chain is a
|
|
262
|
-
* single builder instance (no per-call allocation, like a tagged-template
|
|
263
|
-
* compile target). The terminal `PolicyDef` is read directly off `_def` by
|
|
264
|
-
* `schema_extract.js`.
|
|
265
|
-
*/
|
|
266
273
|
declare class PolicyBuilder {
|
|
267
274
|
readonly _def: PolicyDef;
|
|
268
275
|
constructor(name: string);
|
|
@@ -280,6 +287,34 @@ declare class PolicyBuilder {
|
|
|
280
287
|
to(...roles: string[]): this;
|
|
281
288
|
/** Set the `USING (...)` row-visibility expression (raw SQL). */
|
|
282
289
|
using(sqlExpr: string): this;
|
|
290
|
+
/**
|
|
291
|
+
* "Rows of THIS table whose owner the caller is a member of" — the membership
|
|
292
|
+
* pattern, written so it cannot recurse.
|
|
293
|
+
*
|
|
294
|
+
* THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
|
|
295
|
+
* other: `channels` is visible to members, so its policy reads
|
|
296
|
+
* `channel_members`; `channel_members` is visible to members, so its policy
|
|
297
|
+
* reads `channels`. Postgres refuses the pair at query time with `infinite
|
|
298
|
+
* recursion detected in policy for relation ...`, and the error names the
|
|
299
|
+
* relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
|
|
300
|
+
* is protected by `user_id = auth.uid()` and nothing else, and every other
|
|
301
|
+
* table subqueries INTO it. That shape was in the platform's own schema and
|
|
302
|
+
* written down nowhere; a customer recovered it by reading that schema.
|
|
303
|
+
*
|
|
304
|
+
* `(select auth.uid())` rather than a bare call: the scalar subquery is
|
|
305
|
+
* evaluated ONCE per statement instead of per row.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* // channels: visible to members. The membership table gets the simple one.
|
|
309
|
+
* policy("member_read").for("select").to("authenticated")
|
|
310
|
+
* .memberOf("channel_members", "channel_id")
|
|
311
|
+
* // → id IN (SELECT "channel_id" FROM "channel_members"
|
|
312
|
+
* // WHERE "user_id" = (select auth.uid()))
|
|
313
|
+
*/
|
|
314
|
+
memberOf(membershipTable: string, foreignKey: string, options?: {
|
|
315
|
+
column?: string;
|
|
316
|
+
userColumn?: string;
|
|
317
|
+
}): this;
|
|
283
318
|
/** Set the `WITH CHECK (...)` write-validation expression (raw SQL). */
|
|
284
319
|
withCheck(sqlExpr: string): this;
|
|
285
320
|
/** Set the policy mode: `"permissive"` (default, OR-combined) or
|
|
@@ -647,6 +682,9 @@ type RowShape<T extends TableDef> = {
|
|
|
647
682
|
/** A typed table accessor that mirrors the runtime DBClient surface. */
|
|
648
683
|
interface TypedTable<T extends TableDef> {
|
|
649
684
|
insert(data: InsertShape<T>): Promise<RowShape<T>>;
|
|
685
|
+
upsert(data: InsertShape<T>, opts: {
|
|
686
|
+
onConflict: readonly string[];
|
|
687
|
+
}): Promise<RowShape<T>>;
|
|
650
688
|
/** Update the row by id; resolves to the updated row, or `null` if no row
|
|
651
689
|
* matched (absent or RLS-hidden) — an idempotent outcome, mirroring
|
|
652
690
|
* `findById`. The runtime returns a null row rather than throwing. */
|
|
@@ -755,6 +793,16 @@ type RecommendParamsTyped<T extends TableTypes> = SimilarParamsTyped<T> & {
|
|
|
755
793
|
/** Temel tablo erişimcisi — search'süz beş op. */
|
|
756
794
|
interface EnvTypedTableBase<T extends TableTypes> {
|
|
757
795
|
insert(data: T["insert"]): Promise<T["row"]>;
|
|
796
|
+
/**
|
|
797
|
+
* Insert the row, or update it when it collides on `onConflict`.
|
|
798
|
+
*
|
|
799
|
+
* The conflict columns must carry a unique constraint or index — that is what
|
|
800
|
+
* Postgres matches on — and they are excluded from the update, since they are
|
|
801
|
+
* what matched.
|
|
802
|
+
*/
|
|
803
|
+
upsert(data: T["insert"], opts: {
|
|
804
|
+
onConflict: readonly Extract<keyof T["row"], string>[];
|
|
805
|
+
}): Promise<T["row"]>;
|
|
758
806
|
/** Update the row by id; resolves to the updated row, or `null` if no row
|
|
759
807
|
* matched (absent or RLS-hidden) — an idempotent outcome, mirroring
|
|
760
808
|
* `findById`. The runtime returns a null row rather than throwing. */
|
|
@@ -856,6 +904,27 @@ interface EnvTypedDatabase extends Omit<DBClient, "txPlan" | "asService"> {
|
|
|
856
904
|
* });
|
|
857
905
|
*/
|
|
858
906
|
transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T): Promise<Materialized<T>>;
|
|
907
|
+
/**
|
|
908
|
+
* Run `fn` inside a SAVEPOINT, so a write that fails in it does not poison the
|
|
909
|
+
* rest of the request.
|
|
910
|
+
*
|
|
911
|
+
* A request is ONE Postgres transaction: a failed statement aborts it and
|
|
912
|
+
* every later one answers `current transaction is aborted`. That is why
|
|
913
|
+
* "insert, catch the unique violation, update instead" cannot be written
|
|
914
|
+
* directly — and why {@link EnvTypedTableBase.upsert} exists for the common
|
|
915
|
+
* case. Reach for `attempt` when the recovery is not an upsert.
|
|
916
|
+
*
|
|
917
|
+
* The handle is a parameter, not the ambient `Database`: only what `tx` writes
|
|
918
|
+
* is inside the boundary, so a concurrent branch of the same request cannot be
|
|
919
|
+
* rolled back by someone else's failure.
|
|
920
|
+
*
|
|
921
|
+
* @example
|
|
922
|
+
* const claimed = await Database.attempt(async (tx) => {
|
|
923
|
+
* await tx.insert("seats", { row: 4, seat: 12, user_id: user.id });
|
|
924
|
+
* return true;
|
|
925
|
+
* }).catch(() => false);
|
|
926
|
+
*/
|
|
927
|
+
attempt<T>(fn: (tx: Omit<DBClient, "attempt" | "txPlan" | "asService">) => Promise<T>): Promise<T>;
|
|
859
928
|
/**
|
|
860
929
|
* Return a sibling that bypasses RLS by running as the `service_role`. Use
|
|
861
930
|
* sparingly and explicitly — the default `Database.*` path is RLS-enforced.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tables, TableTypes } from './db/env.js';
|
|
2
|
-
import { D as DBClient, b8 as TxPlanHandle, bh as TxTable, M as Materialized } from './endpoint-
|
|
2
|
+
import { D as DBClient, b8 as TxPlanHandle, bh as TxTable, M as Materialized } from './endpoint-CVWXh6oG.js';
|
|
3
3
|
|
|
4
4
|
/** On delete action for foreign key references. */
|
|
5
5
|
type OnDeleteAction = 'cascade' | 'set null' | 'restrict' | 'no action';
|
|
@@ -36,6 +36,17 @@ interface ColumnDef {
|
|
|
36
36
|
enumName?: string;
|
|
37
37
|
enumValues?: string[];
|
|
38
38
|
unique?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The value is written by the DATABASE — a trigger, a rule, an identity — not by
|
|
41
|
+
* the author and not by a DEFAULT this schema declares. It makes the column
|
|
42
|
+
* optional on INSERT without putting a DEFAULT in the DDL.
|
|
43
|
+
*
|
|
44
|
+
* Before this existed the only way to keep a trigger-filled column off the
|
|
45
|
+
* INSERT type was to give it a fake `default()`: a value the schema claimed to
|
|
46
|
+
* write and the trigger immediately overwrote. That made the schema lie about
|
|
47
|
+
* its own data.
|
|
48
|
+
*/
|
|
49
|
+
dbAssigned?: boolean;
|
|
39
50
|
/** vector(n): the declared dimension count — part of the TYPE (typmod), read
|
|
40
51
|
* by the wire serializer and the deploy's auto-index (FR-001). */
|
|
41
52
|
dimensions?: number;
|
|
@@ -80,6 +91,18 @@ declare class ColumnBuilder<K extends ColumnType = ColumnType, N extends boolean
|
|
|
80
91
|
defaultRandom(): ColumnBuilder<K, N, true, E, P>;
|
|
81
92
|
/** Timestamp: default to now(). */
|
|
82
93
|
defaultNow(): ColumnBuilder<K, N, true, E, P>;
|
|
94
|
+
/**
|
|
95
|
+
* The DATABASE assigns this column's value — a trigger, a rule, an identity.
|
|
96
|
+
*
|
|
97
|
+
* The column becomes optional on INSERT (the author has nothing to send) while
|
|
98
|
+
* the DDL stays free of a DEFAULT this schema would not honour. It is NOT
|
|
99
|
+
* `default()`: that declares a value the schema promises to write.
|
|
100
|
+
*
|
|
101
|
+
* Naming: deliberately not `generated()`. Postgres has GENERATED columns and
|
|
102
|
+
* they are a different thing; borrowing the word would send a reader — or a
|
|
103
|
+
* model writing a schema — to the wrong feature.
|
|
104
|
+
*/
|
|
105
|
+
dbAssigned(): ColumnBuilder<K, N, true, E, P>;
|
|
83
106
|
/** Add a foreign key reference. */
|
|
84
107
|
/**
|
|
85
108
|
* Declares that this column used to be called `previous`.
|
|
@@ -247,22 +270,6 @@ interface PolicyDef {
|
|
|
247
270
|
withCheck: string | null;
|
|
248
271
|
permissive: boolean;
|
|
249
272
|
}
|
|
250
|
-
/**
|
|
251
|
-
* Fluent RLS policy builder.
|
|
252
|
-
*
|
|
253
|
-
* Defaults (documented, applied at construction):
|
|
254
|
-
* - `command`: `"all"` — applies to every SQL command unless `.for(...)` narrows it.
|
|
255
|
-
* - `roles`: `["authenticated"]` — the common case is "rule applies to signed-in
|
|
256
|
-
* users". Call `.to(...)` to override; pass `.to()` with no roles (or never
|
|
257
|
-
* call it after a reset) to target PUBLIC.
|
|
258
|
-
* - `using` / `withCheck`: `null` — no row filter / write check until set.
|
|
259
|
-
* - `permissive`: `true` — `AS PERMISSIVE` (policies OR together).
|
|
260
|
-
*
|
|
261
|
-
* Each method mutates `_def` in place and returns `this`, so the chain is a
|
|
262
|
-
* single builder instance (no per-call allocation, like a tagged-template
|
|
263
|
-
* compile target). The terminal `PolicyDef` is read directly off `_def` by
|
|
264
|
-
* `schema_extract.js`.
|
|
265
|
-
*/
|
|
266
273
|
declare class PolicyBuilder {
|
|
267
274
|
readonly _def: PolicyDef;
|
|
268
275
|
constructor(name: string);
|
|
@@ -280,6 +287,34 @@ declare class PolicyBuilder {
|
|
|
280
287
|
to(...roles: string[]): this;
|
|
281
288
|
/** Set the `USING (...)` row-visibility expression (raw SQL). */
|
|
282
289
|
using(sqlExpr: string): this;
|
|
290
|
+
/**
|
|
291
|
+
* "Rows of THIS table whose owner the caller is a member of" — the membership
|
|
292
|
+
* pattern, written so it cannot recurse.
|
|
293
|
+
*
|
|
294
|
+
* THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
|
|
295
|
+
* other: `channels` is visible to members, so its policy reads
|
|
296
|
+
* `channel_members`; `channel_members` is visible to members, so its policy
|
|
297
|
+
* reads `channels`. Postgres refuses the pair at query time with `infinite
|
|
298
|
+
* recursion detected in policy for relation ...`, and the error names the
|
|
299
|
+
* relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
|
|
300
|
+
* is protected by `user_id = auth.uid()` and nothing else, and every other
|
|
301
|
+
* table subqueries INTO it. That shape was in the platform's own schema and
|
|
302
|
+
* written down nowhere; a customer recovered it by reading that schema.
|
|
303
|
+
*
|
|
304
|
+
* `(select auth.uid())` rather than a bare call: the scalar subquery is
|
|
305
|
+
* evaluated ONCE per statement instead of per row.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* // channels: visible to members. The membership table gets the simple one.
|
|
309
|
+
* policy("member_read").for("select").to("authenticated")
|
|
310
|
+
* .memberOf("channel_members", "channel_id")
|
|
311
|
+
* // → id IN (SELECT "channel_id" FROM "channel_members"
|
|
312
|
+
* // WHERE "user_id" = (select auth.uid()))
|
|
313
|
+
*/
|
|
314
|
+
memberOf(membershipTable: string, foreignKey: string, options?: {
|
|
315
|
+
column?: string;
|
|
316
|
+
userColumn?: string;
|
|
317
|
+
}): this;
|
|
283
318
|
/** Set the `WITH CHECK (...)` write-validation expression (raw SQL). */
|
|
284
319
|
withCheck(sqlExpr: string): this;
|
|
285
320
|
/** Set the policy mode: `"permissive"` (default, OR-combined) or
|
|
@@ -647,6 +682,9 @@ type RowShape<T extends TableDef> = {
|
|
|
647
682
|
/** A typed table accessor that mirrors the runtime DBClient surface. */
|
|
648
683
|
interface TypedTable<T extends TableDef> {
|
|
649
684
|
insert(data: InsertShape<T>): Promise<RowShape<T>>;
|
|
685
|
+
upsert(data: InsertShape<T>, opts: {
|
|
686
|
+
onConflict: readonly string[];
|
|
687
|
+
}): Promise<RowShape<T>>;
|
|
650
688
|
/** Update the row by id; resolves to the updated row, or `null` if no row
|
|
651
689
|
* matched (absent or RLS-hidden) — an idempotent outcome, mirroring
|
|
652
690
|
* `findById`. The runtime returns a null row rather than throwing. */
|
|
@@ -755,6 +793,16 @@ type RecommendParamsTyped<T extends TableTypes> = SimilarParamsTyped<T> & {
|
|
|
755
793
|
/** Temel tablo erişimcisi — search'süz beş op. */
|
|
756
794
|
interface EnvTypedTableBase<T extends TableTypes> {
|
|
757
795
|
insert(data: T["insert"]): Promise<T["row"]>;
|
|
796
|
+
/**
|
|
797
|
+
* Insert the row, or update it when it collides on `onConflict`.
|
|
798
|
+
*
|
|
799
|
+
* The conflict columns must carry a unique constraint or index — that is what
|
|
800
|
+
* Postgres matches on — and they are excluded from the update, since they are
|
|
801
|
+
* what matched.
|
|
802
|
+
*/
|
|
803
|
+
upsert(data: T["insert"], opts: {
|
|
804
|
+
onConflict: readonly Extract<keyof T["row"], string>[];
|
|
805
|
+
}): Promise<T["row"]>;
|
|
758
806
|
/** Update the row by id; resolves to the updated row, or `null` if no row
|
|
759
807
|
* matched (absent or RLS-hidden) — an idempotent outcome, mirroring
|
|
760
808
|
* `findById`. The runtime returns a null row rather than throwing. */
|
|
@@ -856,6 +904,27 @@ interface EnvTypedDatabase extends Omit<DBClient, "txPlan" | "asService"> {
|
|
|
856
904
|
* });
|
|
857
905
|
*/
|
|
858
906
|
transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T): Promise<Materialized<T>>;
|
|
907
|
+
/**
|
|
908
|
+
* Run `fn` inside a SAVEPOINT, so a write that fails in it does not poison the
|
|
909
|
+
* rest of the request.
|
|
910
|
+
*
|
|
911
|
+
* A request is ONE Postgres transaction: a failed statement aborts it and
|
|
912
|
+
* every later one answers `current transaction is aborted`. That is why
|
|
913
|
+
* "insert, catch the unique violation, update instead" cannot be written
|
|
914
|
+
* directly — and why {@link EnvTypedTableBase.upsert} exists for the common
|
|
915
|
+
* case. Reach for `attempt` when the recovery is not an upsert.
|
|
916
|
+
*
|
|
917
|
+
* The handle is a parameter, not the ambient `Database`: only what `tx` writes
|
|
918
|
+
* is inside the boundary, so a concurrent branch of the same request cannot be
|
|
919
|
+
* rolled back by someone else's failure.
|
|
920
|
+
*
|
|
921
|
+
* @example
|
|
922
|
+
* const claimed = await Database.attempt(async (tx) => {
|
|
923
|
+
* await tx.insert("seats", { row: 4, seat: 12, user_id: user.id });
|
|
924
|
+
* return true;
|
|
925
|
+
* }).catch(() => false);
|
|
926
|
+
*/
|
|
927
|
+
attempt<T>(fn: (tx: Omit<DBClient, "attempt" | "txPlan" | "asService">) => Promise<T>): Promise<T>;
|
|
859
928
|
/**
|
|
860
929
|
* Return a sibling that bypasses RLS by running as the `service_role`. Use
|
|
861
930
|
* sparingly and explicitly — the default `Database.*` path is RLS-enforced.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Buckets, BucketTypes } from './stack.cjs';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
-
import { C as CacheClient, P as PalbaseDocsClient, a as PalbaseFlagsClient, L as Logger, b as PalbaseNotificationsClient, c as PalbaseRealtimeClient, D as DBClient, S as SecretsService, d as PalbaseStorageClient, e as PalbaseBucketClient, T as TxPlanBody,
|
|
4
|
-
import { E as EnvTypedDatabase } from './index-
|
|
5
|
-
import { R as RouteMeta } from './registry-
|
|
3
|
+
import { C as CacheClient, P as PalbaseDocsClient, a as PalbaseFlagsClient, L as Logger, b as PalbaseNotificationsClient, c as PalbaseRealtimeClient, D as DBClient, S as SecretsService, d as PalbaseStorageClient, e as PalbaseBucketClient, f as DBOps, T as TxPlanBody, g as TxPlanResponse } from './endpoint-c9h5jriX.cjs';
|
|
4
|
+
import { E as EnvTypedDatabase } from './index-By8Dle5U.cjs';
|
|
5
|
+
import { R as RouteMeta } from './registry-CqPK2Qby.cjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* runtime.ts — request-scoped service singletons.
|
|
@@ -361,14 +361,41 @@ declare function createLazyTransaction(sql: SqlDriver, role: string, claimsJson:
|
|
|
361
361
|
type LazyTransaction = ReturnType<typeof createLazyTransaction>;
|
|
362
362
|
/** Either a live driver transaction or the lazy holder above. */
|
|
363
363
|
type TxLike = SqlTx | LazyTransaction;
|
|
364
|
+
/** What `findMany` accepts beside its filter: an ordering and a row ceiling.
|
|
365
|
+
* Both used to require dropping to raw SQL, and the docs said so — which is how
|
|
366
|
+
* a tenant's controllers filled up with hand-written SELECTs. */
|
|
367
|
+
interface FindManyOptions {
|
|
368
|
+
orderBy?: {
|
|
369
|
+
column: string;
|
|
370
|
+
direction?: "asc" | "desc";
|
|
371
|
+
};
|
|
372
|
+
limit?: number;
|
|
373
|
+
}
|
|
364
374
|
/** The six string-keyed operations, plus an interactive `transaction`. */
|
|
365
375
|
declare function createOps(tx: TxLike): {
|
|
366
376
|
query(sql: string, params?: unknown[]): Promise<Row[]>;
|
|
367
377
|
insert(table: string, data: Row): Promise<Row>;
|
|
378
|
+
/**
|
|
379
|
+
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
380
|
+
*
|
|
381
|
+
* WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
|
|
382
|
+
* unique violation, update instead" does not work here: a request runs in ONE
|
|
383
|
+
* Postgres transaction, so the failed insert aborts it and every later
|
|
384
|
+
* statement answers `current transaction is aborted`. A tenant measured that
|
|
385
|
+
* as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
|
|
386
|
+
* trigger create the row instead — a workaround that needs a new trigger for
|
|
387
|
+
* every table with a unique row.
|
|
388
|
+
*
|
|
389
|
+
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
390
|
+
* so writing them back is at best a no-op and at worst a surprise.
|
|
391
|
+
*/
|
|
392
|
+
upsert(table: string, data: Row, opts: {
|
|
393
|
+
onConflict: readonly string[];
|
|
394
|
+
}): Promise<Row>;
|
|
368
395
|
update(table: string, id: string, data: Row): Promise<Row | null>;
|
|
369
396
|
delete(table: string, id: string): Promise<void>;
|
|
370
397
|
findById(table: string, id: string): Promise<Row | null>;
|
|
371
|
-
findMany(table: string, query?: Row): Promise<Row[]>;
|
|
398
|
+
findMany(table: string, query?: Row, opts?: FindManyOptions): Promise<Row[]>;
|
|
372
399
|
/**
|
|
373
400
|
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
374
401
|
* (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
|
|
@@ -476,6 +503,24 @@ declare function createOps(tx: TxLike): {
|
|
|
476
503
|
supersede(table: string, id: string, row: Row): Promise<Row>;
|
|
477
504
|
/** A real SAVEPOINT inside the request's transaction. */
|
|
478
505
|
transaction<T>(cb: (t: unknown) => Promise<T>): Promise<T>;
|
|
506
|
+
/**
|
|
507
|
+
* Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
|
|
508
|
+
* rolls back only what that handle wrote and the request can keep writing.
|
|
509
|
+
*
|
|
510
|
+
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
511
|
+
* ... Database.insert(...) ... })`, with no parameter — would have to point
|
|
512
|
+
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
513
|
+
* concurrent with itself: `Promise.all([Database.insert(a),
|
|
514
|
+
* Database.attempt(...)])` would put `a` inside the savepoint and roll it
|
|
515
|
+
* back with it. Silent data loss, and the same interleaving this file already
|
|
516
|
+
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
517
|
+
* you can see in the code that crosses it.
|
|
518
|
+
*
|
|
519
|
+
* Postgres, not us: the savepoint is released on success and rolled back on
|
|
520
|
+
* failure by the driver, so an aborted statement inside `fn` does not poison
|
|
521
|
+
* the surrounding transaction.
|
|
522
|
+
*/
|
|
523
|
+
attempt<T>(fn: (tx: DBOps) => Promise<T>): Promise<T>;
|
|
479
524
|
/**
|
|
480
525
|
* Execute a whole transaction plan — what `Database.transaction(fn)` builds.
|
|
481
526
|
*
|