palgres 0.3.1 → 0.3.3

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.
@@ -15,9 +15,28 @@ import { type ColumnMap, type TableHandle } from './schema.js';
15
15
  * .to("authenticated")
16
16
  * .using("owner = (select auth.uid())");
17
17
  */
18
+ /**
19
+ * A structured policy expression, as plain data: one node of the tree that a
20
+ * `USING`, `WITH CHECK` or CHECK expression is made of.
21
+ *
22
+ * You do not write these nodes by hand. Build them with the context passed to a
23
+ * table's `policies` (or `checks`) callback: `p.col(...)`, `p.auth.uid()`,
24
+ * `p.existsIn(...)` and the methods of {@link PolicyExprRef}. Unlike a raw SQL
25
+ * string, a structured expression has its column names checked at compile time,
26
+ * and `auth.uid()` is always written as `(SELECT auth.uid())`.
27
+ */
18
28
  export type PolicyExpr = {
19
29
  kind: "col";
20
30
  name: string;
31
+ /**
32
+ * Which table the column belongs to. When absent, the column is one of
33
+ * the policy's own table. When set, it is the alias of the table inside
34
+ * an `existsIn` subquery.
35
+ *
36
+ * This is what keeps a correlated subquery correct: without it,
37
+ * `team_id = team_id` would compare a column with itself, be true for
38
+ * every row, and the policy would let every row through.
39
+ */
21
40
  alias?: string;
22
41
  } | {
23
42
  kind: "authUid";
@@ -43,13 +62,51 @@ export type PolicyExpr = {
43
62
  expr: PolicyExpr;
44
63
  negated: boolean;
45
64
  } | {
65
+ /**
66
+ * A correlated `EXISTS (SELECT 1 FROM <table> <alias> WHERE <predicate>)`
67
+ * subquery.
68
+ *
69
+ * The subquery is written directly into the policy, not wrapped in a
70
+ * `SECURITY DEFINER` function, so the row-level security of the table it
71
+ * reads still applies inside it: it cannot reveal rows the caller could
72
+ * not read anyway.
73
+ */
46
74
  kind: "exists";
47
75
  table: string;
48
76
  alias: string;
77
+ /**
78
+ * The name of the policy's own table. The correlation needs it.
79
+ *
80
+ * Inside the subquery, Postgres resolves an unqualified column name to
81
+ * the inner table. A column of the policy's own table is therefore
82
+ * written qualified with this name (for example
83
+ * `"public"."documents"."team_id"`).
84
+ * Left unqualified, it would compare the inner column with itself
85
+ * (`pb_e1.team_id = pb_e1.team_id`), be true for every row, and every
86
+ * user would see every row.
87
+ */
49
88
  outer: string;
50
89
  predicate: PolicyExpr;
51
90
  };
91
+ /**
92
+ * The comparison operators of a policy expression: `eq` (`=`), `neq` (`<>`),
93
+ * `gt` (`>`), `gte` (`>=`), `lt` (`<`) and `lte` (`<=`). `in` takes a list on
94
+ * its right-hand side; no builder method produces it, and neither `check()` nor
95
+ * `policySQL()` accepts it.
96
+ */
52
97
  export type PolicyBinOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in";
98
+ /**
99
+ * A policy expression you can keep building on.
100
+ *
101
+ * You get one from the policy context (`p.col("owner")`, `p.auth.uid()`,
102
+ * `p.existsIn(...)`) and chain comparisons and logic onto it: `.eq()`,
103
+ * `.isNull()`, `.and()`, `.not()` and the rest. Every method returns a new
104
+ * `PolicyExprRef`; the expression itself stays plain data in `expr`. Pass the
105
+ * result to `.using()`, `.withCheck()` or `check()`.
106
+ *
107
+ * @example
108
+ * p.col("owner").eq(p.auth.uid()).and(p.col("deleted_at").isNull())
109
+ */
53
110
  export declare class PolicyExprRef {
54
111
  readonly expr: PolicyExpr;
55
112
  constructor(expr: PolicyExpr);
@@ -60,47 +117,216 @@ export declare class PolicyExprRef {
60
117
  gte(other: PolicyOperand): PolicyExprRef;
61
118
  lt(other: PolicyOperand): PolicyExprRef;
62
119
  lte(other: PolicyOperand): PolicyExprRef;
120
+ /**
121
+ * `IS NULL`, or `IS NOT NULL` when `negated` is `true`.
122
+ *
123
+ * Use this rather than `.eq(null)`: in SQL, `= NULL` is never true, it is
124
+ * always unknown. A common use is a partial index or policy condition such as
125
+ * `deleted_at IS NULL`.
126
+ *
127
+ * @param negated `true` for `IS NOT NULL`. Default `false`.
128
+ */
63
129
  isNull(negated?: boolean): PolicyExprRef;
64
130
  and(...others: PolicyExprRef[]): PolicyExprRef;
65
131
  or(...others: PolicyExprRef[]): PolicyExprRef;
66
132
  not(): PolicyExprRef;
67
133
  }
134
+ /**
135
+ * A type annotation for a table used in `existsIn`, for when two table
136
+ * declarations refer to each other.
137
+ *
138
+ * Example: `memberships.household_id` references `households.id`, and the
139
+ * policy of `households` uses `existsIn(memberships, ...)`. Each `const` then
140
+ * needs the other to be initialized, and TypeScript gives up on both with
141
+ * error TS7022 and types them as `any`. That is worse than an error message:
142
+ * with `m.col(...)` typed as `any`, a misspelled column name in the policy
143
+ * compiles silently, and a row-level-security rule fails open. Only a type
144
+ * annotation on one of the two declarations breaks the cycle.
145
+ *
146
+ * `TableRef` is that annotation. It is the table-level counterpart of
147
+ * `references((): AnyColumn => y.id)`, but unlike `AnyColumn` it keeps the
148
+ * column names:
149
+ *
150
+ * ```ts
151
+ * const memberships: TableRef<"id" | "household_id" | "user_id", "memberships"> =
152
+ * defineTable("memberships", {
153
+ * columns: {
154
+ * id: uuid().primaryKey(),
155
+ * household_id: uuid().references(() => households.id),
156
+ * user_id: text().notNull(),
157
+ * },
158
+ * });
159
+ * ```
160
+ *
161
+ * Because the names stay in `Cols`, `m.col("user")` still does not compile, and
162
+ * because `N` is given, the table keeps its key inside `defineSchema`. There is
163
+ * deliberately no looser `AnyTable` type that drops the column names: it would
164
+ * silence the cycle and quietly turn off column checking in your policies.
165
+ *
166
+ * @typeParam Cols The table's column names.
167
+ * @typeParam N The table's name.
168
+ */
68
169
  export type TableRef<Cols extends string, N extends string = string> = TableHandle<Record<Cols, ColumnMap[string]>, N>;
170
+ /**
171
+ * The right-hand side of a comparison such as `.eq()`: another expression or a
172
+ * plain value.
173
+ *
174
+ * A plain value is always a literal, never a column: `.eq("owner")` compares
175
+ * with the string `'owner'`, not with the `owner` column. To compare with a
176
+ * column, write `.eq(p.col("owner"))`.
177
+ */
69
178
  export type PolicyOperand = PolicyExprRef | string | number | boolean | null;
179
+ /**
180
+ * The context for writing an expression over one table's columns. A table's
181
+ * `policies`, `checks`, `indexes` and `foreignKeys` callbacks receive it.
182
+ *
183
+ * Column names are limited to the table's own columns, so a misspelled column
184
+ * is a compile error. This works because those options are callbacks, not
185
+ * plain arrays: TypeScript knows the table's columns when it types the
186
+ * callback, while a bare `policy("x")` could not know which table it is on.
187
+ *
188
+ * @typeParam Cols The table's column names.
189
+ */
70
190
  export interface PolicyExprCtx<Cols extends string = string> {
71
- /** Bu tablonun bir kolonu. Bilinmeyen ad DERLENMEZ. */
191
+ /** A column of this table. An unknown column name does not compile. */
72
192
  col(name: Cols): PolicyExprRef;
193
+ /**
194
+ * "Does this row belong to a team I am a member of?": membership-based
195
+ * multi-tenancy, as a correlated `EXISTS` subquery on another table.
196
+ *
197
+ * ```ts
198
+ * policies: (p) => [
199
+ * policy("team_read").for("select").using(
200
+ * p.existsIn(teamMembers, (m) =>
201
+ * m.col("team_id").eq(p.col("team_id"))
202
+ * .and(m.col("user_id").eq(p.auth.uid()))),
203
+ * ),
204
+ * ]
205
+ * ```
206
+ *
207
+ * `m` gives the columns of the membership table, `p` those of this table.
208
+ * Both are typed, and they are qualified differently in SQL. If they were
209
+ * mixed up, `team_id = team_id` would compare a column with itself and the
210
+ * policy would let every row through.
211
+ *
212
+ * Tables that refer to each other: if the membership table references this
213
+ * table and this table uses `existsIn` on it, annotate one declaration with
214
+ * {@link TableRef}. Otherwise TypeScript types both as `any` (error TS7022)
215
+ * and `m.col(...)` silently accepts any name.
216
+ *
217
+ * Recursion: the membership table's own policy must not use `existsIn` back
218
+ * into this table; give it the simple rule `user_id = auth.uid()`. If two
219
+ * tables' policies query each other, Postgres fails at query time with
220
+ * `infinite recursion detected in policy for relation ...`. Keep it one-way:
221
+ * the membership table has the simple policy, and every other table
222
+ * subqueries into it.
223
+ *
224
+ * @param table The table to look in, for example the membership table.
225
+ * @param predicate Builds the `WHERE` condition of the subquery from that
226
+ * table's columns (`m`) and this table's (`p`).
227
+ */
73
228
  existsIn<C extends ColumnMap, N extends string>(table: TableHandle<C, N>, predicate: (row: PolicyExprCtx<Extract<keyof C, string>>) => PolicyExprRef): PolicyExprRef;
229
+ /**
230
+ * The caller. `p.auth.uid()` is the signed-in user's id. It is written into
231
+ * SQL as `(SELECT auth.uid())`, so Postgres evaluates it once per statement
232
+ * rather than once per row. Not allowed in a CHECK constraint.
233
+ */
74
234
  auth: {
75
235
  uid(): PolicyExprRef;
76
236
  };
237
+ /** A plain value, for saying explicitly that one side of a comparison is a literal. */
77
238
  lit(v: string | number | boolean | null): PolicyExprRef;
78
239
  and(...parts: PolicyExprRef[]): PolicyExprRef;
79
240
  or(...parts: PolicyExprRef[]): PolicyExprRef;
80
241
  not(part: PolicyExprRef): PolicyExprRef;
81
242
  }
243
+ /**
244
+ * Builds an expression context ({@link PolicyExprCtx}) by hand.
245
+ *
246
+ * `defineTable` builds one for you, with the table's own column names, and
247
+ * passes it to the `policies`, `checks`, `indexes` and `foreignKeys`
248
+ * callbacks. Call this yourself only to write an expression outside a table
249
+ * definition.
250
+ *
251
+ * @param _columns The table's column names. Only their type is used: it limits
252
+ * `col()` to these names.
253
+ * @param depth How deep inside `existsIn` subqueries this context is: `0` for
254
+ * the policy's own table. Leave it at the default.
255
+ * @param ownTable The name of the policy's own table, needed for `existsIn`.
256
+ */
82
257
  export declare function exprCtx<Cols extends string = string>(_columns?: readonly Cols[], depth?: number,
258
+ /**
259
+ * The name of the policy's own table. It stays the same at every nesting
260
+ * depth.
261
+ *
262
+ * Inside a subquery, a column without an alias belongs to the policy's own
263
+ * table and is written qualified with this name. That holds for `existsIn`
264
+ * nested inside `existsIn` too, so the policy's own columns can be used at
265
+ * any depth. Without it, an expression that uses `existsIn` cannot be turned
266
+ * into SQL.
267
+ */
83
268
  ownTable?: string): PolicyExprCtx<Cols>;
84
269
  /** The SQL command a policy applies to. `"all"` covers SELECT/INSERT/UPDATE/DELETE. */
85
270
  export type PolicyCommand = "all" | "select" | "insert" | "update" | "delete";
86
271
  /** Whether a policy is permissive (OR-combined, the default) or restrictive
87
272
  * (AND-combined). Mirrors Postgres `CREATE POLICY ... AS PERMISSIVE|RESTRICTIVE`. */
88
273
  export type PolicyMode = "permissive" | "restrictive";
274
+ /**
275
+ * A finished row-level-security policy, as plain serializable data. `policy()`
276
+ * builds one (it is the builder's `_def`); a table's `policies` callback may
277
+ * also return one directly.
278
+ *
279
+ * - `name`: the policy name, unique per table.
280
+ * - `command`: the SQL command it applies to (`FOR` clause).
281
+ * - `roles`: the database roles it applies to (`TO` clause). An empty array
282
+ * means the policy applies to PUBLIC (all roles), the Postgres default.
283
+ * - `using`: the `USING (...)` row-visibility expression, or `null` when none.
284
+ * - `withCheck`: the `WITH CHECK (...)` write-validation expression, or `null`.
285
+ * - `permissive`: `true` for `AS PERMISSIVE` (default), `false` for restrictive.
286
+ */
89
287
  export interface PolicyDef {
90
288
  name: string;
91
289
  command: PolicyCommand;
92
290
  roles: string[];
291
+ /** A structured expression (preferred) or a raw SQL string, or `null` for none. */
93
292
  using: string | PolicyExpr | null;
94
293
  withCheck: string | PolicyExpr | null;
95
294
  permissive: boolean;
96
295
  /** false: the columns it reads get no index of their own from it (withoutIndexes()). */
97
296
  indexes?: false;
98
297
  }
298
+ /**
299
+ * The Postgres roles a policy can apply to, passed to `.to(...)`.
300
+ *
301
+ * The roles Palbase creates are named literally, so a typo such as
302
+ * `"authenitcated"` is a compile error instead of reaching a migration. Two
303
+ * more forms exist for a database Palbase took over, whose policies may name
304
+ * roles of its own: `"public"` (every role) and `` `role:${name}` `` (a role
305
+ * by its own name); `palgres pull` writes them.
306
+ *
307
+ * You do not need to name the `backend_*` roles: Palbase adds them for you.
308
+ * They are still accepted, so code that names them keeps compiling.
309
+ */
99
310
  export type PolicyRole = "anon" | "authenticated" | "service_role" | "backend_anon" | "backend_authenticated" | "backend_service_role"
100
311
  /** Every role (Postgres' PUBLIC): a database taken over may have a policy for it (palgres pull writes it). */
101
312
  | "public"
102
313
  /** A database role by its own name: what a database taken over has a policy for (palgres pull writes it). */
103
314
  | `role:${string}`;
315
+ /**
316
+ * Fluent row-level-security policy builder, returned by {@link policy}.
317
+ *
318
+ * Defaults:
319
+ * - `command`: `"all"`: applies to every SQL command unless `.for(...)` narrows it.
320
+ * - `roles`: `["authenticated"]`: the common case is "this rule applies to
321
+ * signed-in users". Call `.to(...)` to change it; `.to()` with no roles
322
+ * targets PUBLIC (all roles).
323
+ * - `using` / `withCheck`: `null`: no row filter and no write check until set.
324
+ * - `permissive`: `true`: `AS PERMISSIVE` (permissive policies OR together).
325
+ *
326
+ * Each method changes the policy in place and returns the same builder, so
327
+ * calls chain. The finished {@link PolicyDef} is in `_def`; put the builder
328
+ * itself in a table's `policies` list.
329
+ */
104
330
  export declare class PolicyBuilder {
105
331
  readonly _def: PolicyDef;
106
332
  constructor(name: string);
@@ -116,6 +342,23 @@ export declare class PolicyBuilder {
116
342
  * policy("p").to() // PUBLIC
117
343
  */
118
344
  to(...roles: PolicyRole[]): this;
345
+ /**
346
+ * The `USING (...)` expression: which existing rows the policy lets the
347
+ * caller see (and update or delete).
348
+ *
349
+ * Prefer a structured expression, built with the context the `policies`
350
+ * callback receives. Its column names are checked at compile time:
351
+ *
352
+ * ```ts
353
+ * policies: (p) => [
354
+ * policy("owner_read").for("select").using(p.col("owner").eq(p.auth.uid())),
355
+ * ]
356
+ * ```
357
+ *
358
+ * A raw SQL string is also accepted, as an escape hatch for what the
359
+ * structured form cannot express. A string gets no column-name checking: a
360
+ * mistake in it shows up only when Postgres runs `CREATE POLICY` at deploy.
361
+ */
119
362
  using(expr: string | PolicyExprRef): this;
120
363
  /**
121
364
  * "Rows of THIS table whose owner the caller is a member of" — the membership
@@ -145,6 +388,12 @@ export declare class PolicyBuilder {
145
388
  column?: string;
146
389
  userColumn?: string;
147
390
  }): this;
391
+ /**
392
+ * The `WITH CHECK (...)` expression: rows written by an `INSERT` or `UPDATE`
393
+ * must satisfy it, or the write is rejected. Takes the same two forms as
394
+ * `using()`: a structured expression (preferred, column names checked at
395
+ * compile time) or a raw SQL string.
396
+ */
148
397
  withCheck(expr: string | PolicyExprRef): this;
149
398
  /**
150
399
  * The columns this policy reads get no index of their own from it (Palbase
@@ -166,10 +415,50 @@ export declare class PolicyBuilder {
166
415
  * `(table, name)`, so names must be unique per table.
167
416
  */
168
417
  export declare function policy(name: string): PolicyBuilder;
418
+ /**
419
+ * A named CHECK constraint on a table: its name and its SQL expression.
420
+ *
421
+ * `check()` returns one, with the typed expression already compiled to SQL. A
422
+ * table's `checks` may also be a plain array of these, with `expr` written as
423
+ * raw SQL. Both forms end up the same. A deploy matches CHECKs by name and
424
+ * compares `expr` as written (ignoring leading and trailing spaces): the same
425
+ * text is left alone, and changed text replaces the constraint — `n > 0` and
426
+ * `(n > 0)` count as a change.
427
+ */
169
428
  export interface CheckDef {
170
429
  name: string;
430
+ /** The SQL expression. `check()` compiles it for you; in a plain `checks` array you write it yourself. */
171
431
  expr: string;
172
432
  }
433
+ /**
434
+ * `check("<name>", <expression>)`: a typed CHECK constraint.
435
+ *
436
+ * The expression is written with the same builder as a policy expression; this
437
+ * DSL has one expression language, not two. Column names come from the context
438
+ * the table's `checks` callback receives, so a misspelled name does not
439
+ * compile. With a raw SQL string, the same mistake shows up only when Postgres
440
+ * runs `ALTER TABLE ... ADD CONSTRAINT` at deploy.
441
+ *
442
+ * A CHECK may only read the current row's columns: `existsIn(...)`,
443
+ * `auth.uid()` and the `in` operator are rejected with an error when the table
444
+ * is declared.
445
+ *
446
+ * ```ts
447
+ * defineTable("payments", {
448
+ * columns: { amount: integer().notNull(), currency: text().notNull() },
449
+ * checks: (c) => [
450
+ * check("payments_amount_pos", c.col("amount").gt(0)),
451
+ * check("payments_currency_set", c.col("currency").isNull(true)),
452
+ * ],
453
+ * })
454
+ * ```
455
+ *
456
+ * @param name The constraint name; it must not be empty. Palbase matches CHECK
457
+ * constraints by name (a changed expression means DROP and ADD),
458
+ * so the name must be unique per table.
459
+ * @param expr The condition every row must satisfy.
460
+ * @returns The constraint, with its expression compiled to SQL.
461
+ */
173
462
  export declare function check(name: string, expr: PolicyExprRef): CheckDef;
174
463
  /**
175
464
  * The SQL of a policy expression: a raw string as it is, a structure rendered.
@@ -1,3 +1,37 @@
1
+ /**
2
+ * A named raw-SQL DDL object declared in a schema file (`db/public.ts`,
3
+ * `db/billing.ts`, …) for what the typed DSL cannot express: an index method
4
+ * other than btree (`gin`, `gist`, `brin`, `hash`), an opclass, `EXCLUDE`
5
+ * constraints, views, and PL/pgSQL functions.
6
+ *
7
+ * REACH FOR THE TYPED FIELD FIRST. These are declarative and should not come
8
+ * through here:
9
+ * - a CHECK constraint → `checks: [{ name, expr }]`, or the typed
10
+ * `checks: (c) => [check(name, c.col("n").gt(0))]`
11
+ * - a partial index → `index(n).on(…).where(…)`
12
+ * - an expression index → `index(n).onExpression("lower(email)")`
13
+ * - a covering index → `index(n).on(…).include([…])`
14
+ * - a UNIQUE or PARTIAL UNIQUE index → `index(n).unique().where(…)`
15
+ * - a composite FOREIGN KEY → `foreignKeys: (c) => [foreignKey(n).on(…).references(…)]`
16
+ * Those are diffed, validated against the live database before the push, and
17
+ * their mistakes answer you where you write them. A `raw()` is none of that.
18
+ *
19
+ * The deploy runs `up` verbatim on the privileged DDL connection — the same
20
+ * trust as `policy().using()`. It is matched by `name` across deploys:
21
+ * - the same text (leading and trailing spaces aside) → nothing happens;
22
+ * - an index whose text changes under the same name → refused: an index is
23
+ * built `CONCURRENTLY` next to the one serving, so give the new one its own
24
+ * name, and the old one goes at complete;
25
+ * - anything else whose text changes → applied again (after its old `drop`,
26
+ * unless the new text says `CREATE OR REPLACE`).
27
+ * A raw index removed from the schema is dropped at complete.
28
+ *
29
+ * There is no teardown option on the TABLE-level form: a `drop` belongs to a
30
+ * schema-level unit (`RawUnitDef`, below), whose deploy runs it, and
31
+ * `defineTable` refuses a table-level `raw()` that carries one. A trigger, a
32
+ * rule or a view needs a `drop` — a rollback has to be able to take it back —
33
+ * so declare it as a schema-level unit.
34
+ */
1
35
  export interface RawConstraintDef {
2
36
  name: string;
3
37
  up: string;
@@ -36,6 +70,28 @@ export interface RawUnitDef extends RawConstraintDef {
36
70
  */
37
71
  before?: boolean;
38
72
  }
73
+ /**
74
+ * Declares a named raw-SQL unit, for what the typed DSL cannot express.
75
+ *
76
+ * With two arguments it is the table-level form ({@link RawConstraintDef}),
77
+ * for `defineTable`'s `raw: [...]`. With an `options` argument it is the
78
+ * schema-level form ({@link RawUnitDef}), for
79
+ * `defineSchema(name, { tables, raw: [...] })`: `drop` is the SQL that tears
80
+ * the unit down, and `before: true` makes it before the schema's tables.
81
+ *
82
+ * @param name The unit's name. It identifies the unit from one deploy to the
83
+ * next, so two units in one schema cannot share it.
84
+ * @param up The SQL the deploy runs, verbatim, to create the unit.
85
+ * @param options Schema-level units only: `drop` and `before`.
86
+ * @returns The unit, to place in a `raw: [...]` list.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * raw("monthly_totals",
91
+ * `CREATE MATERIALIZED VIEW public.monthly_totals AS SELECT … FROM public.entries GROUP BY …`,
92
+ * { drop: "DROP MATERIALIZED VIEW public.monthly_totals" })
93
+ * ```
94
+ */
39
95
  export declare function raw(name: string, up: string): RawConstraintDef;
40
96
  export declare function raw(name: string, up: string, options: {
41
97
  drop?: string;