@rebasepro/server-postgres 0.14.1 → 0.14.2-canary.g27a129e
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/{auth-users-columns-C-FDnL_e.js → auth-users-columns-JJ8ngvy5.js} +59 -17
- package/dist/auth-users-columns-JJ8ngvy5.js.map +1 -0
- package/dist/{backup-service-BH0Dzo_h.js → backup-service-czK-OAuG.js} +2 -2
- package/dist/{backup-service-BH0Dzo_h.js.map → backup-service-czK-OAuG.js.map} +1 -1
- package/dist/{ensure-collection-policies-DoHwhVf8.js → ensure-collection-policies-D5PtQLyR.js} +3 -3
- package/dist/{ensure-collection-policies-DoHwhVf8.js.map → ensure-collection-policies-D5PtQLyR.js.map} +1 -1
- package/dist/{ensure-collection-tables-DT2eq859.js → ensure-collection-tables-BHUjQ-z4.js} +3 -3
- package/dist/{ensure-collection-tables-DT2eq859.js.map → ensure-collection-tables-BHUjQ-z4.js.map} +1 -1
- package/dist/index.es.js +485 -19
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-69hYT8nr.js → rls-bootstrap-sql-B5Sajku6.js} +2 -2
- package/dist/{rls-bootstrap-sql-69hYT8nr.js.map → rls-bootstrap-sql-B5Sajku6.js.map} +1 -1
- package/dist/{rls-enforcement-gUNDfm7l.js → rls-enforcement-BDBfuTD4.js} +4 -3
- package/dist/rls-enforcement-BDBfuTD4.js.map +1 -0
- package/dist/services/FetchService.d.ts +22 -0
- package/dist/{src-DCdn3Val.js → src-BBFsDaeA.js} +60 -2
- package/dist/src-BBFsDaeA.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +168 -1
- package/dist/utils/pg-error-utils.d.ts +27 -0
- package/dist/{websocket-D2jXv0Ds.js → websocket-BVgDVO-V.js} +2 -2
- package/dist/{websocket-D2jXv0Ds.js.map → websocket-BVgDVO-V.js.map} +1 -1
- package/package.json +6 -6
- package/src/services/FetchService.ts +155 -15
- package/src/services/PersistService.ts +23 -2
- package/src/utils/drizzle-conditions.ts +594 -1
- package/src/utils/pg-error-utils.ts +49 -4
- package/dist/auth-users-columns-C-FDnL_e.js.map +0 -1
- package/dist/rls-enforcement-gUNDfm7l.js.map +0 -1
- package/dist/src-DCdn3Val.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { CollectionConfig, FilterValues, WhereFilterOp, LogicalCondition, FilterCondition, ResolvedRelation, ResolvedForeignKeyOnTarget, ResolvedManyToMany } from "@rebasepro/types";
|
|
3
|
+
import { CollectionConfig, FilterValues, WhereFilterOp, LogicalCondition, FilterCondition, ResolvedRelation, ResolvedBelongsTo, ResolvedForeignKeyOnTarget, ResolvedManyToMany, type RelationAggregateSort } from "@rebasepro/types";
|
|
4
4
|
import { type SearchColumnSpec } from "../schema/search-column";
|
|
5
5
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
6
|
/**
|
|
@@ -82,6 +82,52 @@ export interface FilterCompilationOptions {
|
|
|
82
82
|
*/
|
|
83
83
|
sourceIdColumn?: AnyPgColumn;
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* What a filter field turns out to name.
|
|
87
|
+
*
|
|
88
|
+
* A field naming a column compiles to a comparison on it. A field naming a
|
|
89
|
+
* relation that owns no column here compiles to a whole `EXISTS` condition
|
|
90
|
+
* instead, so there is no column to hand back — which is why resolution
|
|
91
|
+
* answers with a discriminated result rather than a column. The caller cannot
|
|
92
|
+
* tell the two apart from the field name, and the difference is not cosmetic:
|
|
93
|
+
* one is `column <op> value`, the other is a correlated subquery.
|
|
94
|
+
*/
|
|
95
|
+
type FilterTarget = {
|
|
96
|
+
kind: "column";
|
|
97
|
+
column: AnyPgColumn;
|
|
98
|
+
} | {
|
|
99
|
+
/** A path *inside* a json/jsonb column — `metadata->>country`. */
|
|
100
|
+
kind: "json";
|
|
101
|
+
column: AnyPgColumn;
|
|
102
|
+
/** The keys to walk, outermost first. Always at least one. */
|
|
103
|
+
path: string[];
|
|
104
|
+
} | {
|
|
105
|
+
kind: "relation";
|
|
106
|
+
relation: ResolvedForeignKeyOnTarget | ResolvedManyToMany;
|
|
107
|
+
/** Bound here so the compile step cannot be reached without them. */
|
|
108
|
+
registry: PostgresCollectionRegistry;
|
|
109
|
+
sourceIdColumn: AnyPgColumn;
|
|
110
|
+
} | {
|
|
111
|
+
/**
|
|
112
|
+
* A *column of the related row* — `applications.status`. Same `EXISTS`
|
|
113
|
+
* as `relation`, with the predicate moved off the target's id and onto
|
|
114
|
+
* one of its columns.
|
|
115
|
+
*/
|
|
116
|
+
kind: "relation-field";
|
|
117
|
+
/**
|
|
118
|
+
* `via` is not among them: it is refused at resolution, and leaving it
|
|
119
|
+
* in the type would let a later edit reach the compile step with a
|
|
120
|
+
* relation there is no correlation for.
|
|
121
|
+
*/
|
|
122
|
+
relation: ResolvedBelongsTo | ResolvedForeignKeyOnTarget | ResolvedManyToMany;
|
|
123
|
+
registry: PostgresCollectionRegistry;
|
|
124
|
+
/** The column on *this* table the subquery correlates back to. */
|
|
125
|
+
sourceIdColumn: AnyPgColumn;
|
|
126
|
+
/** The table the predicate is asked of, already resolved. */
|
|
127
|
+
targetTable: PgTable<any>;
|
|
128
|
+
/** The column on {@link targetTable} the predicate compares. */
|
|
129
|
+
targetColumn: AnyPgColumn;
|
|
130
|
+
};
|
|
85
131
|
/** Drizzle dynamic query builder — accepts innerJoin + where chaining */
|
|
86
132
|
export interface DrizzleDynamicQuery {
|
|
87
133
|
innerJoin(table: PgTable<any>, condition: SQL): this;
|
|
@@ -166,6 +212,23 @@ export declare class DrizzleConditionBuilder {
|
|
|
166
212
|
* rather than a half-built condition.
|
|
167
213
|
*/
|
|
168
214
|
private static resolveFilterTarget;
|
|
215
|
+
/**
|
|
216
|
+
* `applications.status` — the relation, and the column of the target it
|
|
217
|
+
* addresses.
|
|
218
|
+
*
|
|
219
|
+
* `undefined` when the first segment names no relation: the field simply is
|
|
220
|
+
* not a relation path, and resolution carries on to the guesses and then to
|
|
221
|
+
* the unknown-field answer, which is where a typo belongs. A segment that
|
|
222
|
+
* *does* name a relation is a different matter — the author plainly meant
|
|
223
|
+
* this shape — so everything after that point throws rather than returning,
|
|
224
|
+
* naming what went wrong. Falling through would report "unknown filter
|
|
225
|
+
* field 'applications.status'" and list the columns of the wrong table.
|
|
226
|
+
*
|
|
227
|
+
* `via` is refused for the reason it is absent from
|
|
228
|
+
* `filterableRelationKinds`: its join path is authored source → target with
|
|
229
|
+
* no stated inverse, so there is nothing to correlate a subquery back to.
|
|
230
|
+
*/
|
|
231
|
+
private static resolveRelationFieldTarget;
|
|
169
232
|
/**
|
|
170
233
|
* Build filter conditions from FilterValues
|
|
171
234
|
*/
|
|
@@ -278,6 +341,109 @@ export declare class DrizzleConditionBuilder {
|
|
|
278
341
|
* a relation are exactly the six below.
|
|
279
342
|
*/
|
|
280
343
|
private static buildRelationFilterPredicate;
|
|
344
|
+
/**
|
|
345
|
+
* A filter on a *column of the related row* — `applications.status`.
|
|
346
|
+
*
|
|
347
|
+
* The same `EXISTS` {@link buildRelationFilterCondition} builds, with the
|
|
348
|
+
* predicate moved off the target's id and onto one of its columns:
|
|
349
|
+
*
|
|
350
|
+
* EXISTS (SELECT 1 FROM talent_applications AS t
|
|
351
|
+
* WHERE t.talent_id = talents.id
|
|
352
|
+
* AND t.status IN ('applied', 'reviewing', 'interview'))
|
|
353
|
+
*
|
|
354
|
+
* which is the shape every "who is waiting" queue is written in. Without
|
|
355
|
+
* it the only way to ask is to fetch every row and filter in the browser,
|
|
356
|
+
* and a filter the client applies after paging is not a filter — the page
|
|
357
|
+
* was already chosen without it.
|
|
358
|
+
*
|
|
359
|
+
* A many-to-many needs one more table than the id filter does. That one
|
|
360
|
+
* stops at the junction, because the junction already holds the value it
|
|
361
|
+
* compares; a column of the target is a table further out, so the subquery
|
|
362
|
+
* joins the target to the junction and correlates from the junction. The
|
|
363
|
+
* join is inside `EXISTS`, so it cannot multiply the outer rows the way a
|
|
364
|
+
* top-level join through a junction would.
|
|
365
|
+
*
|
|
366
|
+
* `belongsTo` is included even though its foreign key is a column here:
|
|
367
|
+
* `author.name` is a column of another table either way, and refusing the
|
|
368
|
+
* one relation kind that reads most naturally would be a rule about
|
|
369
|
+
* implementation rather than about meaning.
|
|
370
|
+
*
|
|
371
|
+
* Under RLS the subquery runs as the reader, so it sees the target rows
|
|
372
|
+
* that reader's policies allow and no others. On the positive direction
|
|
373
|
+
* that is exactly right. On the negative — `!=`, `not-in`, and any
|
|
374
|
+
* `NOT EXISTS` — "no related row satisfies this" and "no related row this
|
|
375
|
+
* reader can see satisfies this" are the same sentence, so a target table
|
|
376
|
+
* with row-level security and no `SELECT` policy for `rebase_user` makes
|
|
377
|
+
* every row look unmatched and the negative filter over-reports. Nothing is
|
|
378
|
+
* leaked: the outer table's own policies still decide which rows exist. The
|
|
379
|
+
* cause is a missing policy on the target rather than anything here, and it
|
|
380
|
+
* is the same caveat the id-filter path carries.
|
|
381
|
+
*/
|
|
382
|
+
static buildRelationFieldCondition(target: Extract<FilterTarget, {
|
|
383
|
+
kind: "relation-field";
|
|
384
|
+
}>, op: WhereFilterOp, value: unknown, field: string, collectionPath: string): SQL;
|
|
385
|
+
/**
|
|
386
|
+
* The inner predicate of a relation *column* filter, and whether the
|
|
387
|
+
* `EXISTS` wrapping it is negated.
|
|
388
|
+
*
|
|
389
|
+
* The negation rule is the one {@link buildRelationFilterPredicate} states
|
|
390
|
+
* and holds for exactly the same reason, one column over. A negative
|
|
391
|
+
* operator is `NOT EXISTS` of the **positive** predicate, never `EXISTS` of
|
|
392
|
+
* a negated one: `EXISTS (… AND status != 'hired')` asks "does some
|
|
393
|
+
* application differ from hired", which is true of nearly every candidate
|
|
394
|
+
* with more than one application and answers nothing anybody asked.
|
|
395
|
+
* `NOT EXISTS (… AND status = 'hired')` asks "is there no hired
|
|
396
|
+
* application", which is what unticking a value means — and it makes `==`
|
|
397
|
+
* and `!=` partition the rows, the way a filter implies they do.
|
|
398
|
+
*
|
|
399
|
+
* `is-null` and `is-not-null` are the exception, and deliberately not a
|
|
400
|
+
* complementary pair here. On a column they compile to `EXISTS (… AND col
|
|
401
|
+
* IS NULL)` and `EXISTS (… AND col IS NOT NULL)` — "has a related row whose
|
|
402
|
+
* column is unset" and "has one where it is set" — which is the plain
|
|
403
|
+
* reading of `applications.status is-not-null` and the useful one. They are
|
|
404
|
+
* both true of a candidate with two applications, one of each. Making
|
|
405
|
+
* `is-not-null` the negation instead would make it "no application has an
|
|
406
|
+
* unset status", which is true of a candidate with no applications at all
|
|
407
|
+
* and so answers a queue with the very rows the queue exists to exclude.
|
|
408
|
+
*
|
|
409
|
+
* Unlike the id path, every operator is available: the compared value is an
|
|
410
|
+
* ordinary column, so `>=` on a date and `ilike` on a name mean here what
|
|
411
|
+
* they mean anywhere else. Only an operator that does not exist is refused,
|
|
412
|
+
* and it throws rather than returning `null` — a dropped condition widens
|
|
413
|
+
* the read, which is the whole reason this file fails closed.
|
|
414
|
+
*/
|
|
415
|
+
private static buildRelationColumnPredicate;
|
|
416
|
+
/**
|
|
417
|
+
* An aggregate over the rows a relation reaches, as a scalar expression —
|
|
418
|
+
* what `orderBy: [{ relation: "applications", field: "created_at", agg:
|
|
419
|
+
* "min" }, "asc"]` compiles to.
|
|
420
|
+
*
|
|
421
|
+
* (SELECT min(t.created_at) FROM talent_applications AS t
|
|
422
|
+
* WHERE t.talent_id = talents.id)
|
|
423
|
+
*
|
|
424
|
+
* A correlated scalar subquery rather than a `LEFT JOIN LATERAL`: the join
|
|
425
|
+
* would have to be threaded into a query the relational query builder
|
|
426
|
+
* assembles, while a scalar expression drops straight into `ORDER BY` and
|
|
427
|
+
* into the keyset comparison behind cursor paging — which has to be the
|
|
428
|
+
* *same* expression, or paging and ordering disagree and rows are skipped.
|
|
429
|
+
*
|
|
430
|
+
* `correlateTo` is what the subquery is pinned against. Left out, it is the
|
|
431
|
+
* outer row's key column and the expression is correlated in the ordinary
|
|
432
|
+
* way. Given a literal — the cursor row's id — the subquery stops being
|
|
433
|
+
* correlated at all, so Postgres evaluates it once for the whole statement
|
|
434
|
+
* rather than per row. That is how a cursor pages over an aggregate it has
|
|
435
|
+
* no stored value for: the value is recomputed from the id it does have.
|
|
436
|
+
*
|
|
437
|
+
* Over zero related rows `count` is 0 and every other function is NULL,
|
|
438
|
+
* which is what puts "nobody waiting" at a defined end of the order rather
|
|
439
|
+
* than wherever a missing value would land. See `buildOrderExpressions` for
|
|
440
|
+
* where that end is pinned.
|
|
441
|
+
*
|
|
442
|
+
* Under RLS the subquery runs as the reader, so a related row the reader
|
|
443
|
+
* cannot see does not contribute — an aggregate is over the rows that
|
|
444
|
+
* reader can see, which is the only total it could honestly report.
|
|
445
|
+
*/
|
|
446
|
+
static buildRelationAggregateExpression(spec: RelationAggregateSort, table: PgTable<any>, collection: CollectionConfig, registry: PostgresCollectionRegistry, sourceIdColumn: AnyPgColumn, collectionPath: string, correlateTo?: unknown): SQL;
|
|
281
447
|
/** The column a table's rows are keyed by: its primary key, else `id`. */
|
|
282
448
|
private static primaryKeyColumn;
|
|
283
449
|
/**
|
|
@@ -491,3 +657,4 @@ export declare class DrizzleConditionBuilder {
|
|
|
491
657
|
* This allows code to use PostgresConditionBuilder alongside future MongoConditionBuilder, etc.
|
|
492
658
|
*/
|
|
493
659
|
export declare const PostgresConditionBuilder: typeof DrizzleConditionBuilder;
|
|
660
|
+
export {};
|
|
@@ -73,6 +73,33 @@ export declare function classifyConnectFailure(error: unknown): ConnectFailure;
|
|
|
73
73
|
* error to the Studio SQL Editor user.
|
|
74
74
|
*/
|
|
75
75
|
export declare function isRoleSwitchingPermissionError(error: unknown): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Was this `42501` the *caller* being refused by a policy, rather than the
|
|
78
|
+
* server lacking a privilege?
|
|
79
|
+
*
|
|
80
|
+
* Both arrive as `insufficient_privilege`, and they are opposite kinds of
|
|
81
|
+
* problem. A row-level-security refusal is a working access-control system
|
|
82
|
+
* doing its job: the caller asked for something their policies do not permit,
|
|
83
|
+
* which is a 403 and nobody's bug. A missing `GRANT` is the deployment being
|
|
84
|
+
* wrong — the connection role cannot touch the table at all, no policy is
|
|
85
|
+
* involved, and nothing the caller changes about the request will help.
|
|
86
|
+
*
|
|
87
|
+
* Postgres distinguishes them in the message, so this does too:
|
|
88
|
+
*
|
|
89
|
+
* new row violates row-level security policy for table "notes" → the caller
|
|
90
|
+
* permission denied for table notes → the server
|
|
91
|
+
*
|
|
92
|
+
* Only writes reach this. A read that RLS excludes is not an error — the rows
|
|
93
|
+
* are filtered and the caller gets an empty page — so the erroring case is
|
|
94
|
+
* specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.
|
|
95
|
+
*
|
|
96
|
+
* Matched on the message because that is the only thing carrying the
|
|
97
|
+
* distinction; the SQLSTATE is identical either way. Narrow by design: anything
|
|
98
|
+
* not naming row-level security stays the server's problem, since reporting a
|
|
99
|
+
* genuine privilege misconfiguration as "forbidden" would send an operator
|
|
100
|
+
* hunting for a policy bug that does not exist.
|
|
101
|
+
*/
|
|
102
|
+
export declare function isRowLevelSecurityDenial(error: unknown): boolean;
|
|
76
103
|
/**
|
|
77
104
|
* Translate a raw PostgreSQL error into a user-friendly message.
|
|
78
105
|
*
|
|
@@ -3,7 +3,7 @@ import process from "process";
|
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { c as __exportAll } from "./connection-BuZ97wsr.js";
|
|
5
5
|
import { n as resolveClientListLimit, r as ANONYMOUS_USER_ID, t as ListLimitError } from "./data_driver-ULAyJEi9.js";
|
|
6
|
-
import "./src-
|
|
6
|
+
import "./src-BBFsDaeA.js";
|
|
7
7
|
import { ApiError, assertWriteRequestValid, extractUserFromToken, logger, resolveRequireAuth, safeCompare } from "@rebasepro/server";
|
|
8
8
|
import { WebSocketServer } from "ws";
|
|
9
9
|
import { inspect } from "util";
|
|
@@ -611,4 +611,4 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
611
611
|
//#endregion
|
|
612
612
|
export { websocket_exports as n, createPostgresWebSocket as t };
|
|
613
613
|
|
|
614
|
-
//# sourceMappingURL=websocket-
|
|
614
|
+
//# sourceMappingURL=websocket-BVgDVO-V.js.map
|