@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.1-canary.g06f263c
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/PostgresBootstrapper.d.ts +33 -1
- package/dist/auth/services.d.ts +16 -0
- package/dist/backup-service-vAKWJkYL.js +8867 -0
- package/dist/backup-service-vAKWJkYL.js.map +1 -0
- package/dist/cli-helpers.d.ts +24 -0
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-BjSwj0FM.js +57 -0
- package/dist/ensure-collection-policies-BjSwj0FM.js.map +1 -0
- package/dist/ensure-collection-tables-D6-XhqnQ.js +590 -0
- package/dist/ensure-collection-tables-D6-XhqnQ.js.map +1 -0
- package/dist/history/HistoryService.d.ts +9 -29
- package/dist/index.es.js +965 -9705
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +24 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +116 -1
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/services/FetchService.d.ts +36 -1
- package/dist/services/row-pipeline.d.ts +3 -1
- package/dist/{src-3VmUJ8Xn.js → src-C_NHNVW2.js} +288 -168
- package/dist/src-C_NHNVW2.js.map +1 -0
- package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +157 -3
- package/dist/utils/pg-error-utils.d.ts +25 -3
- package/dist/websocket-DB7TbFPT.js +529 -0
- package/dist/websocket-DB7TbFPT.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +14 -0
- package/src/PostgresBootstrapper.ts +212 -36
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +21 -2
- package/src/cli-helpers.ts +44 -0
- package/src/cli.ts +31 -3
- package/src/collections/buildRegistry.ts +1 -1
- package/src/connection.ts +73 -0
- package/src/databasePoolManager.ts +5 -2
- package/src/history/HistoryService.ts +13 -31
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/dynamic-tables.ts +1 -1
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +142 -25
- package/src/schema/generate-drizzle-schema-logic.ts +26 -7
- package/src/schema/generate-postgres-ddl-logic.ts +335 -16
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +32 -10
- package/src/services/FetchService.ts +79 -11
- package/src/services/RelationService.ts +38 -3
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +3 -1
- package/src/utils/drizzle-conditions.ts +509 -45
- package/src/utils/pg-error-utils.ts +98 -3
- package/src/websocket.ts +3 -3
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-DGMYK0fr.js +0 -304
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
- package/dist/src-3VmUJ8Xn.js.map +0 -1
- package/dist/src-D5xBTl32.js.map +0 -1
|
@@ -3,10 +3,12 @@ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
|
3
3
|
import { CollectionConfig, FilterValues, ResolvedRelation, LogicalCondition, isManyToMany } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
|
-
import {
|
|
6
|
+
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
7
|
+
import { DrizzleConditionBuilder, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
7
8
|
import {
|
|
8
9
|
getCollectionByPath,
|
|
9
10
|
getTableForCollection,
|
|
11
|
+
getPrimaryKeys,
|
|
10
12
|
requirePrimaryKeys,
|
|
11
13
|
deriveRowAddress,
|
|
12
14
|
parseIdValues,
|
|
@@ -45,6 +47,45 @@ export class FetchService {
|
|
|
45
47
|
return query?.[tableName] as RelationalQueryBuilder<TablesRelationalConfig, TableRelationalConfig> | undefined;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The context the condition builder needs to compile a filter key that is
|
|
52
|
+
* not a column name outright.
|
|
53
|
+
*
|
|
54
|
+
* Two such keys. An owning relation's key resolves through the collection's
|
|
55
|
+
* relations to its foreign-key column; a relation whose link lives on the
|
|
56
|
+
* target table or in a junction resolves to a correlated `EXISTS`, which
|
|
57
|
+
* needs the registry to reach that other table and this table's key column
|
|
58
|
+
* to correlate back.
|
|
59
|
+
*
|
|
60
|
+
* Looked up rather than passed: every read path already has the path, only
|
|
61
|
+
* some have the collection, and a path that names no registered collection
|
|
62
|
+
* (a nested/derived one) is not an error here — the builder simply falls
|
|
63
|
+
* back to guessing the default key shapes, and a relation filter it cannot
|
|
64
|
+
* compile stays unresolvable and so fails closed.
|
|
65
|
+
*/
|
|
66
|
+
private filterContext(collectionPath: string, table: PgTable<any>): FilterCompilationOptions {
|
|
67
|
+
const collection = this.registry.getCollectionByPath(collectionPath) ?? undefined;
|
|
68
|
+
return {
|
|
69
|
+
collection,
|
|
70
|
+
registry: this.registry,
|
|
71
|
+
sourceIdColumn: collection ? this.resolveIdColumn(collection, table) : undefined
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The table column this collection's rows are keyed by, or `undefined`.
|
|
77
|
+
*
|
|
78
|
+
* `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
|
|
79
|
+
* resolvable key is not an error on the filter path — it only means the
|
|
80
|
+
* relation filters that would correlate on it cannot be compiled, which
|
|
81
|
+
* the builder already handles by failing that field closed.
|
|
82
|
+
*/
|
|
83
|
+
private resolveIdColumn(collection: CollectionConfig, table: PgTable<any>): AnyPgColumn | undefined {
|
|
84
|
+
const [idInfo] = getPrimaryKeys(collection, this.registry);
|
|
85
|
+
if (!idInfo) return undefined;
|
|
86
|
+
return table[idInfo.fieldName as keyof typeof table] as AnyPgColumn | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
/**
|
|
49
90
|
* Build filter conditions from FilterValues
|
|
50
91
|
* Delegates to DrizzleConditionBuilder.buildFilterConditions
|
|
@@ -54,7 +95,9 @@ export class FetchService {
|
|
|
54
95
|
table: PgTable<any>,
|
|
55
96
|
collectionPath: string
|
|
56
97
|
): SQL[] {
|
|
57
|
-
return DrizzleConditionBuilder.buildFilterConditions(
|
|
98
|
+
return DrizzleConditionBuilder.buildFilterConditions(
|
|
99
|
+
filter, table, collectionPath, this.filterContext(collectionPath, table)
|
|
100
|
+
);
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
// =============================================================
|
|
@@ -64,20 +107,45 @@ export class FetchService {
|
|
|
64
107
|
/**
|
|
65
108
|
* Resolves the correct Drizzle column for sorting.
|
|
66
109
|
* Automatically maps owning relation property keys to their underlying foreign key column.
|
|
110
|
+
*
|
|
111
|
+
* The relation's own `localKey` is the authority for that foreign key, not
|
|
112
|
+
* `<field>_id`. The default local key comes from `generateForeignKeyName`,
|
|
113
|
+
* which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
|
|
114
|
+
* `users` → `user_id` — and an author can override it outright. A wrong
|
|
115
|
+
* guess resolves to nothing, the caller drops the `ORDER BY`, and the rows
|
|
116
|
+
* come back in whatever order Postgres pleases: paging over that repeats
|
|
117
|
+
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
118
|
+
* caller that hands over no collection to resolve against.
|
|
67
119
|
*/
|
|
68
120
|
private resolveOrderByField(
|
|
69
121
|
table: PgTable<any>,
|
|
70
122
|
orderBy: string,
|
|
71
123
|
collection?: CollectionConfig
|
|
72
124
|
): AnyPgColumn | undefined {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
const columnAt = (key: string): AnyPgColumn | undefined =>
|
|
126
|
+
(key in table ? table[key as keyof typeof table] as AnyPgColumn : undefined) || undefined;
|
|
127
|
+
|
|
128
|
+
const direct = columnAt(orderBy);
|
|
129
|
+
if (direct) return direct;
|
|
130
|
+
|
|
131
|
+
// Owning relation, resolved: the relation names its own local key.
|
|
132
|
+
if (collection) {
|
|
133
|
+
const relation = resolveCollectionRelations(collection)[orderBy];
|
|
134
|
+
if (relation?.kind === "belongsTo") {
|
|
135
|
+
const foreignKey = columnAt(relation.localKey);
|
|
136
|
+
if (foreignKey) return foreignKey;
|
|
78
137
|
}
|
|
79
138
|
}
|
|
80
|
-
|
|
139
|
+
|
|
140
|
+
// No collection in hand — the two shapes an owning relation's key takes
|
|
141
|
+
// by default (e.g. `project` → `project_id`, `userProfile` →
|
|
142
|
+
// `user_profile_id`).
|
|
143
|
+
for (const guess of [`${orderBy}_id`, generateForeignKeyName(orderBy)]) {
|
|
144
|
+
const foreignKey = columnAt(guess);
|
|
145
|
+
if (foreignKey) return foreignKey;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return undefined;
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
/**
|
|
@@ -85,7 +153,7 @@ export class FetchService {
|
|
|
85
153
|
* Converts collection relations to a Drizzle-compatible `with` object.
|
|
86
154
|
*
|
|
87
155
|
* When `include` is provided, only those relations are loaded.
|
|
88
|
-
* When `include` is absent, ALL relations are loaded (
|
|
156
|
+
* When `include` is absent, ALL relations are loaded (the admin path).
|
|
89
157
|
*
|
|
90
158
|
* Automatically detects many-to-many junction tables and nests
|
|
91
159
|
* the target relation so actual row data is returned.
|
|
@@ -311,7 +379,7 @@ export class FetchService {
|
|
|
311
379
|
}
|
|
312
380
|
|
|
313
381
|
if (options.logical) {
|
|
314
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
382
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
315
383
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
316
384
|
}
|
|
317
385
|
|
|
@@ -656,7 +724,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
656
724
|
}
|
|
657
725
|
|
|
658
726
|
if (options.logical) {
|
|
659
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
727
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
660
728
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
661
729
|
}
|
|
662
730
|
|
|
@@ -18,6 +18,39 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
|
|
|
18
18
|
import { logger } from "@rebasepro/server";
|
|
19
19
|
import type { NestedPathHop } from "./nested-path";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The ids in a to-many relation write, whatever shape the caller sent.
|
|
23
|
+
*
|
|
24
|
+
* A membership list is written as either the related rows (`[{ id: 1 }]`, what
|
|
25
|
+
* the admin UI sends back after reading them) or as bare keys (`[1]`, `["t-1"]`,
|
|
26
|
+
* what anyone writing the API by hand sends). Only the first was read, via a
|
|
27
|
+
* blind `.map(rel => rel.id)`, and a bare key therefore became `undefined`:
|
|
28
|
+
* on a numeric-keyed target that surfaced as `Invalid numeric ID: undefined`,
|
|
29
|
+
* and on a string-keyed one it did not surface at all — `String(undefined)`
|
|
30
|
+
* wrote a junction row pointing at the literal `"undefined"`, which no read
|
|
31
|
+
* would ever match. Both shapes are accepted here, in one place, because both
|
|
32
|
+
* call sites had the same assumption.
|
|
33
|
+
*
|
|
34
|
+
* An element that carries no key is refused rather than skipped: dropping it
|
|
35
|
+
* would silently write a shorter membership list than the caller asked for.
|
|
36
|
+
*/
|
|
37
|
+
function relationTargetIds(value: unknown, relationName: string, collectionSlug: string): (string | number)[] {
|
|
38
|
+
if (!Array.isArray(value)) return [];
|
|
39
|
+
|
|
40
|
+
return value.map((element, index) => {
|
|
41
|
+
if (typeof element === "string" || typeof element === "number") return element;
|
|
42
|
+
if (element && typeof element === "object") {
|
|
43
|
+
const id = (element as { id?: unknown }).id;
|
|
44
|
+
if (typeof id === "string" || typeof id === "number") return id;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Cannot write relation "${relationName}" on "${collectionSlug}": element ${index} carries no id. ` +
|
|
48
|
+
"Pass either the related rows (`[{ id: … }]`) or their keys (`[1, 2]`), not " +
|
|
49
|
+
`${element === null ? "null" : typeof element}.`
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
/**
|
|
22
55
|
* Typed wrapper for Drizzle dynamic query innerJoin.
|
|
23
56
|
* Drizzle's `$dynamic()` queries lose the `innerJoin` method from
|
|
@@ -840,7 +873,7 @@ export class RelationService {
|
|
|
840
873
|
const relation = findRelation(resolvedRelations, key);
|
|
841
874
|
if (!relation || relation.cardinality !== "many") continue;
|
|
842
875
|
|
|
843
|
-
const targetEntityIds = (value
|
|
876
|
+
const targetEntityIds = relationTargetIds(value, key, collection.slug);
|
|
844
877
|
const targetCollection = relation.target();
|
|
845
878
|
|
|
846
879
|
// Use joinPath if available
|
|
@@ -1166,7 +1199,9 @@ export class RelationService {
|
|
|
1166
1199
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
1167
1200
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
1168
1201
|
const targetIdInfo = targetPks[0];
|
|
1169
|
-
|
|
1202
|
+
// This path already read both shapes; the other two did not.
|
|
1203
|
+
// Same helper now, so the three cannot drift again.
|
|
1204
|
+
const targetEntityIds = relationTargetIds(newValue, relation.relationName, sourceCollection.slug);
|
|
1170
1205
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
1171
1206
|
|
|
1172
1207
|
const newLinks = parsedTargetIds.map(targetId => ({
|
|
@@ -1238,7 +1273,7 @@ export class RelationService {
|
|
|
1238
1273
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
1239
1274
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
1240
1275
|
const targetIdInfo = targetPks[0];
|
|
1241
|
-
const targetEntityIds = (newValue
|
|
1276
|
+
const targetEntityIds = relationTargetIds(newValue, relation.relationName, sourceCollection.slug);
|
|
1242
1277
|
const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
1243
1278
|
|
|
1244
1279
|
const newLinks = parsedTargetIds.map(targetId => ({
|
|
@@ -4,7 +4,7 @@ import { Client as PgClient } from "pg";
|
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
5
|
import { DataService } from "./dataService";
|
|
6
6
|
|
|
7
|
-
import { FetchCollectionProps, ListenCollectionProps, ListenOneProps, DataDriver, CollectionUpdateMessage, SingleUpdateMessage, CollectionPatchMessage, WebSocketMessage, FilterValues, CollectionConfig, RebaseCallContext, resolveClientListLimit } from "@rebasepro/types";
|
|
7
|
+
import { ANONYMOUS_USER_ID, FetchCollectionProps, ListenCollectionProps, ListenOneProps, DataDriver, CollectionUpdateMessage, SingleUpdateMessage, CollectionPatchMessage, WebSocketMessage, FilterValues, CollectionConfig, RebaseCallContext, resolveClientListLimit } from "@rebasepro/types";
|
|
8
8
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
9
9
|
import { sql as drizzleSql } from "drizzle-orm";
|
|
10
10
|
import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces";
|
|
@@ -751,7 +751,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
751
751
|
// Always wrap in a transaction with session vars, defaulting to anonymous context if missing.
|
|
752
752
|
// Refetches are reads: apply the same GUCs + reader-role downgrade as the
|
|
753
753
|
// driver's read path, so realtime cannot leak rows the initial fetch hid.
|
|
754
|
-
const activeAuth = authContext || { uid:
|
|
754
|
+
const activeAuth = authContext || { uid: ANONYMOUS_USER_ID,
|
|
755
755
|
roles: ["anon"] };
|
|
756
756
|
return await this.db.transaction(async (tx) => {
|
|
757
757
|
await applyAuthContext(tx, { uid: activeAuth.uid, roles: activeAuth.roles }, this.rlsUserRole);
|
|
@@ -931,7 +931,7 @@ roles: activeAuth.roles },
|
|
|
931
931
|
|
|
932
932
|
// Always wrap in a transaction with session vars, defaulting to anonymous context if missing.
|
|
933
933
|
// Same read isolation as collection refetches: GUCs + reader-role downgrade.
|
|
934
|
-
const activeAuth = authContext || { uid:
|
|
934
|
+
const activeAuth = authContext || { uid: ANONYMOUS_USER_ID,
|
|
935
935
|
roles: ["anon"] };
|
|
936
936
|
return await this.db.transaction(async (tx) => {
|
|
937
937
|
await applyAuthContext(tx, { uid: activeAuth.uid, roles: activeAuth.roles }, this.rlsUserRole);
|
|
@@ -14,7 +14,9 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
|
|
|
14
14
|
*
|
|
15
15
|
* - `"ref"` — a `{ id, path, __type: "relation" }` reference carrying the
|
|
16
16
|
* target's values. This is what the admin renders.
|
|
17
|
-
* - `"inline"` — the target's own columns, flat. This is what REST serves
|
|
17
|
+
* - `"inline"` — the target's own columns, flat. This is what REST serves, and
|
|
18
|
+
* — since the in-process SDK reads through the same pipeline — what
|
|
19
|
+
* `rebase.data` / `context.data` serve too. A developer never sees a ref.
|
|
18
20
|
*
|
|
19
21
|
* They used to be two functions that happened to agree, and the agreement was
|
|
20
22
|
* not enforced by anything: the row-identity bug had to be fixed five times
|