@rebasepro/server-postgres 0.16.0 → 0.16.1-canary.g0d7af95
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/PostgresAdapter.d.ts +1 -1
- package/dist/PostgresBackendDriver.d.ts +75 -12
- package/dist/PostgresBootstrapper.d.ts +6 -6
- package/dist/auth/services.d.ts +1 -1
- package/dist/backup/backup-cron.d.ts +1 -1
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/index.d.ts +4 -4
- package/dist/{backup-service-BZoixhVl.js → backup-service-BtgHxfFm.js} +5 -4
- package/dist/{backup-service-BZoixhVl.js.map → backup-service-BtgHxfFm.js.map} +1 -1
- package/dist/cli-helpers.d.ts +41 -0
- package/dist/{auth-users-columns-CgyPWQ18.js → collection-index-DxJBvVTH.js} +486 -503
- package/dist/collection-index-DxJBvVTH.js.map +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/collections/validate-relations.d.ts +1 -1
- package/dist/{connection-BuZ97wsr.js → connection-GOKU3Hu5.js} +34 -7
- package/dist/connection-GOKU3Hu5.js.map +1 -0
- package/dist/connection.d.ts +16 -0
- package/dist/data-transformer.d.ts +1 -1
- package/dist/{ensure-collection-policies-BVFb2olB.js → ensure-collection-policies-DFpOl8SM.js} +4 -4
- package/dist/{ensure-collection-policies-BVFb2olB.js.map → ensure-collection-policies-DFpOl8SM.js.map} +1 -1
- package/dist/ensure-collection-tables-DMjOkeRy.js +1952 -0
- package/dist/ensure-collection-tables-DMjOkeRy.js.map +1 -0
- package/dist/index.d.ts +16 -16
- package/dist/index.es.js +19 -7128
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-B5Sajku6.js → rls-bootstrap-sql-DNzaWd4C.js} +3 -3
- package/dist/{rls-bootstrap-sql-B5Sajku6.js.map → rls-bootstrap-sql-DNzaWd4C.js.map} +1 -1
- package/dist/{rls-enforcement-Ch0T6OwW.js → rls-enforcement-CInuYj1-.js} +14 -4
- package/dist/rls-enforcement-CInuYj1-.js.map +1 -0
- package/dist/schema/classify-change.d.ts +82 -0
- package/dist/schema/collection-index.d.ts +182 -0
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +1 -1
- package/dist/schema/ensure-collection-tables.d.ts +93 -2
- package/dist/schema/generate-schema-commit.d.ts +136 -0
- package/dist/schema/generated-schema-staleness.d.ts +19 -0
- package/dist/schema/introspect-db-constraints.d.ts +1 -1
- package/dist/schema/introspect-db-logic.d.ts +3 -3
- package/dist/schema/introspect-db-project.d.ts +1 -1
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-structure.d.ts +2 -2
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/schema/vector-index.d.ts +88 -0
- package/dist/services/BranchService.d.ts +2 -2
- package/dist/services/FetchService.d.ts +4 -4
- package/dist/services/PersistService.d.ts +5 -5
- package/dist/services/RelationService.d.ts +3 -3
- package/dist/services/RelationWriteService.d.ts +3 -3
- package/dist/services/cdc/junction-tables.d.ts +1 -1
- package/dist/services/cdc/trigger-cdc.d.ts +1 -1
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +1 -1
- package/dist/services/channel-bus/index.d.ts +2 -2
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/dataService.d.ts +10 -10
- package/dist/services/index.d.ts +4 -4
- package/dist/services/junction-writes.d.ts +2 -2
- package/dist/services/nested-path.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +3 -3
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/services/write-denial.d.ts +1 -1
- package/dist/{src-BBFsDaeA.js → src-DiDgtX8P.js} +97 -1
- package/dist/src-DiDgtX8P.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +2 -2
- package/dist/websocket-CUnpSe8v.js +8188 -0
- package/dist/websocket-CUnpSe8v.js.map +1 -0
- package/dist/websocket.d.ts +29 -2
- package/package.json +7 -7
- package/src/PostgresBackendDriver.ts +190 -59
- package/src/backup/backup-service.ts +1 -1
- package/src/cli-helpers.ts +117 -2
- package/src/cli.ts +22 -0
- package/src/connection.ts +37 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/classify-change.ts +436 -0
- package/src/schema/collection-index.ts +427 -0
- package/src/schema/ensure-collection-tables.test.ts +168 -1
- package/src/schema/ensure-collection-tables.ts +365 -14
- package/src/schema/generate-drizzle-schema-logic.ts +23 -11
- package/src/schema/generate-drizzle-schema.ts +13 -2
- package/src/schema/generate-postgres-ddl-logic.ts +33 -6
- package/src/schema/generate-postgres-ddl.ts +13 -2
- package/src/schema/generate-schema-commit.ts +242 -0
- package/src/schema/generated-schema-staleness.ts +114 -1
- package/src/schema/vector-index.ts +278 -0
- package/src/services/collection-helpers.ts +3 -2
- package/src/websocket.ts +47 -3
- package/dist/auth-users-columns-CgyPWQ18.js.map +0 -1
- package/dist/connection-BuZ97wsr.js.map +0 -1
- package/dist/data_driver-ULAyJEi9.js +0 -193
- package/dist/data_driver-ULAyJEi9.js.map +0 -1
- package/dist/ensure-collection-tables-BY1pHRD_.js +0 -840
- package/dist/ensure-collection-tables-BY1pHRD_.js.map +0 -1
- package/dist/rls-enforcement-Ch0T6OwW.js.map +0 -1
- package/dist/src-BBFsDaeA.js.map +0 -1
- package/dist/utils/table-classification.d.ts +0 -8
- package/dist/websocket-BVgDVO-V.js +0 -614
- package/dist/websocket-BVgDVO-V.js.map +0 -1
- package/src/utils/table-classification.ts +0 -16
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a `{ type: "vector" }` property becomes an ANN index.
|
|
3
|
+
*
|
|
4
|
+
* Without an index, pgvector answers `ORDER BY embedding <=> $1` by computing
|
|
5
|
+
* the distance to every row and sorting — exact, and linear. That is the right
|
|
6
|
+
* answer at ten thousand rows and the wrong one at a million, which is why the
|
|
7
|
+
* default here is to build an index rather than to leave the column bare.
|
|
8
|
+
*
|
|
9
|
+
* ## The operator class is not a detail
|
|
10
|
+
*
|
|
11
|
+
* An index is built for exactly one operator class, and the planner uses it
|
|
12
|
+
* only for the operator that class implements. `vector_cosine_ops` answers
|
|
13
|
+
* `<=>` and nothing else; a query asking for `<->` against a cosine index gets
|
|
14
|
+
* a sequential scan and no error. So the default indexed distance here is
|
|
15
|
+
* `cosine`, because `cosine` is what `vectorSearch` measures with when the
|
|
16
|
+
* caller does not say — see `DrizzleConditionBuilder.buildVectorSearch`. The
|
|
17
|
+
* two defaults have to agree, and this comment is the reason they do.
|
|
18
|
+
*
|
|
19
|
+
* ## Why 2000 dimensions is a ceiling and not an error
|
|
20
|
+
*
|
|
21
|
+
* pgvector cannot index a `vector` wider than 2000 dimensions with either
|
|
22
|
+
* method. A 3072-dimension embedding (`text-embedding-3-large`) is therefore
|
|
23
|
+
* storable and searchable but not indexable. Refusing the boot over that would
|
|
24
|
+
* make a working configuration unbootable; silently indexing it is impossible.
|
|
25
|
+
* So the column is created, the index is skipped, and the reason is reported.
|
|
26
|
+
*
|
|
27
|
+
* Like `search-column.ts`, this module exists so that the DDL generator and the
|
|
28
|
+
* boot-time ensure render the *same* specification rather than describing the
|
|
29
|
+
* same index twice, differently. `contracts/derived-names.txt` records the
|
|
30
|
+
* names both produce, and CI fails if they diverge.
|
|
31
|
+
*/
|
|
32
|
+
import type { CollectionConfig, Property, VectorDistance, VectorIndexConfig } from "@rebasepro/types";
|
|
33
|
+
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
34
|
+
import { getTableName } from "@rebasepro/common";
|
|
35
|
+
import { toPostgresIdentifier } from "@rebasepro/utils";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The widest `vector` pgvector will build an HNSW or IVFFlat index over.
|
|
39
|
+
* Storage and exact search are unaffected by this limit.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_INDEXABLE_VECTOR_DIMENSIONS = 2000;
|
|
42
|
+
|
|
43
|
+
/** The distance indexed when a property does not name one. */
|
|
44
|
+
export const DEFAULT_VECTOR_DISTANCE: VectorDistance = "cosine";
|
|
45
|
+
|
|
46
|
+
/** The index method used when a property does not name one. */
|
|
47
|
+
export const DEFAULT_VECTOR_INDEX_METHOD = "hnsw" as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Operator class per distance. These strings are part of the database contract:
|
|
51
|
+
* they appear in `CREATE INDEX`, so renaming one renames an index.
|
|
52
|
+
*/
|
|
53
|
+
const OPERATOR_CLASS: Record<VectorDistance, string> = {
|
|
54
|
+
cosine: "vector_cosine_ops",
|
|
55
|
+
l2: "vector_l2_ops",
|
|
56
|
+
inner_product: "vector_ip_ops"
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Short, stable tag per distance, used to name the index. */
|
|
60
|
+
const DISTANCE_TAG: Record<VectorDistance, string> = {
|
|
61
|
+
cosine: "cosine",
|
|
62
|
+
l2: "l2",
|
|
63
|
+
inner_product: "ip"
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface VectorIndexSpec {
|
|
67
|
+
schema: string;
|
|
68
|
+
table: string;
|
|
69
|
+
column: string;
|
|
70
|
+
indexName: string;
|
|
71
|
+
method: "hnsw" | "ivfflat";
|
|
72
|
+
distance: VectorDistance;
|
|
73
|
+
operatorClass: string;
|
|
74
|
+
/** Rendered into `WITH (...)`; empty when every parameter is defaulted. */
|
|
75
|
+
parameters: Array<[string, number]>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A vector column that will not be indexed, and why. */
|
|
79
|
+
export interface SkippedVectorIndex {
|
|
80
|
+
schema: string;
|
|
81
|
+
table: string;
|
|
82
|
+
column: string;
|
|
83
|
+
dimensions: number;
|
|
84
|
+
reason: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface VectorIndexPlan {
|
|
88
|
+
specs: VectorIndexSpec[];
|
|
89
|
+
skipped: SkippedVectorIndex[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class VectorIndexConfigError extends Error {
|
|
93
|
+
constructor(message: string) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = "VectorIndexConfigError";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isVectorProperty = (prop: unknown): prop is Property & {
|
|
100
|
+
type: "vector";
|
|
101
|
+
dimensions: number;
|
|
102
|
+
index?: VectorIndexConfig | false;
|
|
103
|
+
} => !!prop && typeof prop === "object" && (prop as { type?: string }).type === "vector";
|
|
104
|
+
|
|
105
|
+
const asDistances = (config: VectorIndexConfig, label: string): VectorDistance[] => {
|
|
106
|
+
const raw = config.distance ?? DEFAULT_VECTOR_DISTANCE;
|
|
107
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
108
|
+
if (list.length === 0) {
|
|
109
|
+
throw new VectorIndexConfigError(
|
|
110
|
+
`${label}: \`index.distance\` is an empty array. Name at least one distance, or set \`index: false\` to create no index.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const seen = new Set<VectorDistance>();
|
|
114
|
+
for (const distance of list) {
|
|
115
|
+
if (!(distance in OPERATOR_CLASS)) {
|
|
116
|
+
throw new VectorIndexConfigError(
|
|
117
|
+
`${label}: \`index.distance\` is "${distance}", which is not a pgvector distance. Use ${Object.keys(OPERATOR_CLASS).map(d => `"${d}"`).join(", ")}.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (seen.has(distance)) {
|
|
121
|
+
throw new VectorIndexConfigError(`${label}: \`index.distance\` lists "${distance}" twice.`);
|
|
122
|
+
}
|
|
123
|
+
seen.add(distance);
|
|
124
|
+
}
|
|
125
|
+
return list;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const assertPositiveInteger = (value: number | undefined, key: string, label: string): void => {
|
|
129
|
+
if (value === undefined) return;
|
|
130
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
131
|
+
throw new VectorIndexConfigError(
|
|
132
|
+
`${label}: \`index.${key}\` is ${JSON.stringify(value)}. It must be a positive integer.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Index parameters for one method. Parameters belonging to the *other* method
|
|
139
|
+
* are rejected rather than ignored, because a silently dropped `lists` on an
|
|
140
|
+
* HNSW index reads, from the config, exactly like a tuned index.
|
|
141
|
+
*/
|
|
142
|
+
const parametersFor = (
|
|
143
|
+
method: "hnsw" | "ivfflat",
|
|
144
|
+
config: VectorIndexConfig,
|
|
145
|
+
label: string
|
|
146
|
+
): Array<[string, number]> => {
|
|
147
|
+
assertPositiveInteger(config.m, "m", label);
|
|
148
|
+
assertPositiveInteger(config.efConstruction, "efConstruction", label);
|
|
149
|
+
assertPositiveInteger(config.lists, "lists", label);
|
|
150
|
+
|
|
151
|
+
if (method === "hnsw") {
|
|
152
|
+
if (config.lists !== undefined) {
|
|
153
|
+
throw new VectorIndexConfigError(
|
|
154
|
+
`${label}: \`index.lists\` only applies to \`method: "ivfflat"\`. Remove it, or switch the method.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const params: Array<[string, number]> = [];
|
|
158
|
+
if (config.m !== undefined) params.push(["m", config.m]);
|
|
159
|
+
if (config.efConstruction !== undefined) params.push(["ef_construction", config.efConstruction]);
|
|
160
|
+
return params;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const key of ["m", "efConstruction"] as const) {
|
|
164
|
+
if (config[key] !== undefined) {
|
|
165
|
+
throw new VectorIndexConfigError(
|
|
166
|
+
`${label}: \`index.${key}\` only applies to \`method: "hnsw"\`. Remove it, or switch the method.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return config.lists !== undefined ? [["lists", config.lists]] : [];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Every ANN index a collection's vector properties call for.
|
|
175
|
+
*
|
|
176
|
+
* `resolveColumn` is passed in rather than imported so that this module stays
|
|
177
|
+
* free of the DDL generator, which imports *it*. Both callers hand it the same
|
|
178
|
+
* `resolveColumnName`, and a contract test asserts the names agree.
|
|
179
|
+
*/
|
|
180
|
+
export const buildVectorIndexPlan = (
|
|
181
|
+
collection: CollectionConfig,
|
|
182
|
+
resolveColumn: (propName: string, prop?: Property | null) => string
|
|
183
|
+
): VectorIndexPlan => {
|
|
184
|
+
const specs: VectorIndexSpec[] = [];
|
|
185
|
+
const skipped: SkippedVectorIndex[] = [];
|
|
186
|
+
const properties = collection.properties ?? {};
|
|
187
|
+
|
|
188
|
+
const table = getTableName(collection);
|
|
189
|
+
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
190
|
+
|
|
191
|
+
for (const [propName, prop] of Object.entries(properties)) {
|
|
192
|
+
if (!isVectorProperty(prop)) continue;
|
|
193
|
+
if (prop.index === false) continue;
|
|
194
|
+
|
|
195
|
+
const label = `${collection.slug}.${propName}`;
|
|
196
|
+
const column = resolveColumn(propName, prop as Property);
|
|
197
|
+
const config: VectorIndexConfig = prop.index ?? {};
|
|
198
|
+
const method = config.method ?? DEFAULT_VECTOR_INDEX_METHOD;
|
|
199
|
+
|
|
200
|
+
if (method !== "hnsw" && method !== "ivfflat") {
|
|
201
|
+
throw new VectorIndexConfigError(
|
|
202
|
+
`${label}: \`index.method\` is "${method}". Use "hnsw" or "ivfflat".`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Read before the dimension gate so a malformed block is reported even
|
|
207
|
+
// on a column too wide to index — the config is wrong either way.
|
|
208
|
+
const distances = asDistances(config, label);
|
|
209
|
+
const parameters = parametersFor(method, config, label);
|
|
210
|
+
|
|
211
|
+
if (!Number.isInteger(prop.dimensions) || prop.dimensions <= 0) {
|
|
212
|
+
throw new VectorIndexConfigError(
|
|
213
|
+
`${label}: \`dimensions\` is ${JSON.stringify(prop.dimensions)}. It must be a positive integer.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (prop.dimensions > MAX_INDEXABLE_VECTOR_DIMENSIONS) {
|
|
218
|
+
skipped.push({
|
|
219
|
+
schema,
|
|
220
|
+
table,
|
|
221
|
+
column,
|
|
222
|
+
dimensions: prop.dimensions,
|
|
223
|
+
reason:
|
|
224
|
+
`pgvector cannot index a vector wider than ${MAX_INDEXABLE_VECTOR_DIMENSIONS} dimensions, and ` +
|
|
225
|
+
`${label} declares ${prop.dimensions}. The column works and \`vectorSearch\` still answers, as an ` +
|
|
226
|
+
`exact scan. To index it, reduce the dimensions (many embedding models support a shorter output) ` +
|
|
227
|
+
`or set \`index: false\` to state that the scan is intended.`
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const distance of distances) {
|
|
233
|
+
specs.push({
|
|
234
|
+
schema,
|
|
235
|
+
table,
|
|
236
|
+
column,
|
|
237
|
+
// Distance is in the name because one column may carry an index
|
|
238
|
+
// per distance, and two indexes cannot share a name. The method
|
|
239
|
+
// is in it because switching methods is a different index, not
|
|
240
|
+
// a redefinition of the same one — `CREATE INDEX IF NOT EXISTS`
|
|
241
|
+
// would otherwise keep the old one and report success.
|
|
242
|
+
indexName: toPostgresIdentifier(`${table}_${column}_${method}_${DISTANCE_TAG[distance]}`),
|
|
243
|
+
method,
|
|
244
|
+
distance,
|
|
245
|
+
operatorClass: OPERATOR_CLASS[distance],
|
|
246
|
+
parameters
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { specs, skipped };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The `CREATE INDEX` for one spec.
|
|
256
|
+
*
|
|
257
|
+
* `CONCURRENTLY` is deliberately absent, for the same reason it is absent from
|
|
258
|
+
* `searchIndexStatements`: this form is replayed as part of a migration, where
|
|
259
|
+
* a concurrent build is not allowed. The boot-time ensure rewrites it — see
|
|
260
|
+
* `ensureCollectionTables`.
|
|
261
|
+
*/
|
|
262
|
+
export const vectorIndexStatement = (spec: VectorIndexSpec): string => {
|
|
263
|
+
const params = spec.parameters.length
|
|
264
|
+
? ` WITH (${spec.parameters.map(([key, value]) => `${key} = ${value}`).join(", ")})`
|
|
265
|
+
: "";
|
|
266
|
+
return (
|
|
267
|
+
`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" ` +
|
|
268
|
+
`USING ${spec.method} ("${spec.column}" ${spec.operatorClass})${params};`
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** Every statement for a plan, in a stable order. */
|
|
273
|
+
export const vectorIndexStatements = (plan: VectorIndexPlan): string[] =>
|
|
274
|
+
plan.specs.map(vectorIndexStatement);
|
|
275
|
+
|
|
276
|
+
/** The index names a plan creates — what the derived-names contract records. */
|
|
277
|
+
export const vectorIndexNames = (plan: VectorIndexPlan): string[] =>
|
|
278
|
+
plan.specs.map(spec => spec.indexName);
|
|
@@ -217,8 +217,9 @@ isUUID });
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
220
|
+
// A collection that declares no primary key gets the implicit `id` column,
|
|
221
|
+
// which is what the schema generators emit for it — so read it back the
|
|
222
|
+
// same way rather than reporting the table as keyless.
|
|
222
223
|
if (keys.length === 0 && "id" in table) {
|
|
223
224
|
const idCol = table["id" as keyof typeof table] as AnyPgColumn;
|
|
224
225
|
const idMeta = getColumnMeta(idCol);
|
package/src/websocket.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RealtimeService } from "./services/realtimeService";
|
|
2
|
-
import { PostgresBackendDriver } from "./PostgresBackendDriver";
|
|
2
|
+
import { PostgresBackendDriver, effectiveSqlRole } from "./PostgresBackendDriver";
|
|
3
3
|
import type { DataDriver, DeleteProps, FetchCollectionProps, FetchOneProps, SaveProps, TableMetadata, BranchInfo, AuthAdapter } from "@rebasepro/types";
|
|
4
4
|
import { ANONYMOUS_USER_ID, isSQLAdmin, isSchemaAdmin, resolveClientListLimit, ListLimitError } from "@rebasepro/types";
|
|
5
5
|
import type { User } from "@rebasepro/types";
|
|
@@ -77,11 +77,28 @@ const CHANNEL_MESSAGE_TYPES = new Set([
|
|
|
77
77
|
"channel_history"
|
|
78
78
|
]);
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
80
|
+
/**
|
|
81
|
+
* WebSocket message types that require an admin session.
|
|
82
|
+
*
|
|
83
|
+
* Exported so the test can READ it. It used to be private, and the test that
|
|
84
|
+
* exists to make "a privileged verb added without a role check" impossible held
|
|
85
|
+
* a hand-typed copy of the same nine strings — so it agreed with itself, and
|
|
86
|
+
* `FETCH_APPLICATION_ROLES` was added to neither. That verb runs
|
|
87
|
+
* `SELECT DISTINCT unnest(roles)` over the users table through `executeSql`,
|
|
88
|
+
* which is the owner connection and not subject to RLS, so any authenticated
|
|
89
|
+
* non-admin could enumerate every role in the project — and any anonymous
|
|
90
|
+
* socket could, on a deployment with `requireAuth: false`.
|
|
91
|
+
*
|
|
92
|
+
* The list is no longer the whole guarantee. `PUBLIC_TYPES` below is its
|
|
93
|
+
* counterpart, and a test asserts that every `case` this file handles appears
|
|
94
|
+
* in exactly one of the two — so a verb added to neither fails rather than
|
|
95
|
+
* defaulting to reachable.
|
|
96
|
+
*/
|
|
97
|
+
export const ADMIN_ONLY_TYPES = new Set([
|
|
82
98
|
"EXECUTE_SQL",
|
|
83
99
|
"FETCH_DATABASES",
|
|
84
100
|
"FETCH_ROLES",
|
|
101
|
+
"FETCH_APPLICATION_ROLES",
|
|
85
102
|
"FETCH_UNMAPPED_TABLES",
|
|
86
103
|
"FETCH_TABLE_METADATA",
|
|
87
104
|
"FETCH_CURRENT_DATABASE",
|
|
@@ -90,6 +107,23 @@ const ADMIN_ONLY_TYPES = new Set([
|
|
|
90
107
|
"LIST_BRANCHES"
|
|
91
108
|
]);
|
|
92
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Message types deliberately reachable by a non-admin session.
|
|
112
|
+
*
|
|
113
|
+
* Data operations, gated per row by RLS and per request by the same write
|
|
114
|
+
* checks the REST layer applies — not by this list. It exists so that "which
|
|
115
|
+
* bucket is this verb in" is a question with an answer for every verb, and
|
|
116
|
+
* adding one without answering it is a test failure.
|
|
117
|
+
*/
|
|
118
|
+
export const PUBLIC_TYPES = new Set([
|
|
119
|
+
"FETCH_COLLECTION",
|
|
120
|
+
"FETCH_ONE",
|
|
121
|
+
"COUNT",
|
|
122
|
+
"SAVE",
|
|
123
|
+
"DELETE",
|
|
124
|
+
"CHECK_UNIQUE_FIELD"
|
|
125
|
+
]);
|
|
126
|
+
|
|
93
127
|
/**
|
|
94
128
|
* Recursively extract the deepest error message from an error's cause chain (e.g., Drizzle wrapping a PG error).
|
|
95
129
|
*/
|
|
@@ -533,6 +567,16 @@ colors: true }));
|
|
|
533
567
|
sql: typeof sql === "string" ? sql.substring(0, 500) : String(sql),
|
|
534
568
|
database: options?.database,
|
|
535
569
|
role: options?.role,
|
|
570
|
+
// The role the statement asked for is above;
|
|
571
|
+
// this is the one it ran as. They used to be
|
|
572
|
+
// assumed identical, so an execution that fell
|
|
573
|
+
// back to the owner was audited under the role
|
|
574
|
+
// it had failed to assume. That fallback is now
|
|
575
|
+
// an error everywhere except the documented
|
|
576
|
+
// `DISABLE_DB_ROLE_SWITCHING` opt-out — which is
|
|
577
|
+
// precisely the case this line still has to
|
|
578
|
+
// report honestly.
|
|
579
|
+
effectiveRole: effectiveSqlRole(options?.role),
|
|
536
580
|
paramCount: Array.isArray(options?.params) ? options.params.length : 0,
|
|
537
581
|
resultRows: Array.isArray(result) ? result.length : "unknown",
|
|
538
582
|
uid: auditSession?.user?.uid ?? "unknown",
|