@rebasepro/server-postgres 0.17.3-canary.gdd23447 → 0.18.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/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/collection-index-C7LopMyX.js +247 -0
- package/dist/collection-index-C7LopMyX.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
- package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
- package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +19307 -3059
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
- package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js +0 -9044
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js +0 -277
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime introspection — builds collections in memory from the live database.
|
|
3
|
-
*
|
|
4
|
-
* This is what makes BaaS mode work with zero configuration: instead of loading
|
|
5
|
-
* collection files from disk, the server reads `information_schema` at boot and
|
|
6
|
-
* derives a collection per table, so any database is served over REST without a
|
|
7
|
-
* single config file.
|
|
8
|
-
*
|
|
9
|
-
* Distinct from `introspect-db.ts`, which runs the same queries but emits
|
|
10
|
-
* TypeScript *source* for a developer to edit and commit (declared collections). The two
|
|
11
|
-
* share the mapping helpers in `introspect-db-logic.ts` so a table is described
|
|
12
|
-
* the same way whether it was generated or introspected.
|
|
13
|
-
*/
|
|
14
|
-
import type { PostgresCollectionConfig } from "@rebasepro/types";
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
TableRow,
|
|
18
|
-
TableColumn,
|
|
19
|
-
EnumValue,
|
|
20
|
-
PrimaryKeyRow,
|
|
21
|
-
ForeignKeyRow,
|
|
22
|
-
TableMeta,
|
|
23
|
-
buildTablesMap,
|
|
24
|
-
buildEnumMap,
|
|
25
|
-
identifyJoinTables,
|
|
26
|
-
singularize,
|
|
27
|
-
mapPgType,
|
|
28
|
-
getIconForTable
|
|
29
|
-
} from "./introspect-db-logic";
|
|
30
|
-
import { humanize } from "./introspect-db-naming";
|
|
31
|
-
import { firstFreeKey, toWireKey } from "@rebasepro/utils";
|
|
32
|
-
|
|
33
|
-
export interface IntrospectedSchema {
|
|
34
|
-
tablesMap: Map<string, TableMeta>;
|
|
35
|
-
enumMap: Map<string, string[]>;
|
|
36
|
-
joinTables: Set<string>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Whether a table carries an authorization model of its own. */
|
|
40
|
-
export interface TableRlsStatus {
|
|
41
|
-
table: string;
|
|
42
|
-
/** ALTER TABLE … ENABLE ROW LEVEL SECURITY has been run. */
|
|
43
|
-
rlsEnabled: boolean;
|
|
44
|
-
/** Policies attached to it. RLS enabled with none = nothing is visible. */
|
|
45
|
-
policyCount: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Read the RLS posture of each table in a schema.
|
|
50
|
-
*
|
|
51
|
-
* This is what decides whether baas mode may serve a table. A table with RLS
|
|
52
|
-
* disabled has no authorization model: since every authenticated request runs
|
|
53
|
-
* as `rebase_user`, and that role is granted DML on the schema, serving such a
|
|
54
|
-
* table hands every row to every logged-in user.
|
|
55
|
-
*/
|
|
56
|
-
export async function readRlsStatus(client: Queryable, pgSchema: string): Promise<Map<string, TableRlsStatus>> {
|
|
57
|
-
const { rows } = await client.query<{ table: string; rls_enabled: boolean; policy_count: string | number }>(
|
|
58
|
-
`SELECT c.relname AS table,
|
|
59
|
-
c.relrowsecurity AS rls_enabled,
|
|
60
|
-
(SELECT count(*) FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count
|
|
61
|
-
FROM pg_class c
|
|
62
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
63
|
-
WHERE n.nspname = $1 AND c.relkind = 'r'`,
|
|
64
|
-
[pgSchema]
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return new Map(
|
|
68
|
-
rows.map((r) => [
|
|
69
|
-
r.table,
|
|
70
|
-
{ table: r.table, rlsEnabled: r.rls_enabled === true, policyCount: Number(r.policy_count ?? 0) }
|
|
71
|
-
])
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Minimal query surface — satisfied by pg.Client and pg.Pool alike. */
|
|
76
|
-
export interface Queryable {
|
|
77
|
-
query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Read tables, columns, enums, primary keys and foreign keys for a schema.
|
|
82
|
-
* Mirrors the queries in introspect-db.ts.
|
|
83
|
-
*/
|
|
84
|
-
export async function introspectSchema(client: Queryable, pgSchema: string): Promise<IntrospectedSchema> {
|
|
85
|
-
const { rows: tables } = await client.query<TableRow>(
|
|
86
|
-
`SELECT table_name
|
|
87
|
-
FROM information_schema.tables
|
|
88
|
-
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
89
|
-
AND table_name NOT LIKE 'drizzle_%'
|
|
90
|
-
AND table_name NOT LIKE 'rebase_%'
|
|
91
|
-
ORDER BY table_name`,
|
|
92
|
-
[pgSchema]
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const { rows: columns } = await client.query<TableColumn>(
|
|
96
|
-
`SELECT
|
|
97
|
-
c.table_name,
|
|
98
|
-
c.column_name,
|
|
99
|
-
c.data_type,
|
|
100
|
-
c.udt_name,
|
|
101
|
-
c.is_nullable,
|
|
102
|
-
c.column_default,
|
|
103
|
-
(SELECT a.atttypmod FROM pg_attribute a
|
|
104
|
-
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
105
|
-
WHERE pc.relname = c.table_name
|
|
106
|
-
AND a.attname = c.column_name
|
|
107
|
-
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
108
|
-
FROM information_schema.columns c
|
|
109
|
-
WHERE c.table_schema = $1`,
|
|
110
|
-
[pgSchema]
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const { rows: enumValues } = await client.query<EnumValue>(
|
|
114
|
-
`SELECT t.typname AS enum_name,
|
|
115
|
-
e.enumlabel AS enum_value,
|
|
116
|
-
e.enumsortorder AS sort_order
|
|
117
|
-
FROM pg_type t
|
|
118
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
119
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
120
|
-
WHERE n.nspname = $1
|
|
121
|
-
ORDER BY t.typname, e.enumsortorder`,
|
|
122
|
-
[pgSchema]
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const { rows: pks } = await client.query<PrimaryKeyRow>(
|
|
126
|
-
`SELECT t.relname as table_name, a.attname as column_name
|
|
127
|
-
FROM pg_index i
|
|
128
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
129
|
-
AND a.attnum = ANY(i.indkey)
|
|
130
|
-
JOIN pg_class t ON t.oid = i.indrelid
|
|
131
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
132
|
-
WHERE i.indisprimary AND n.nspname = $1`,
|
|
133
|
-
[pgSchema]
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const { rows: fks } = await client.query<ForeignKeyRow>(
|
|
137
|
-
`SELECT
|
|
138
|
-
tc.table_name,
|
|
139
|
-
kcu.column_name,
|
|
140
|
-
ccu.table_name AS foreign_table_name,
|
|
141
|
-
ccu.column_name AS foreign_column_name
|
|
142
|
-
FROM information_schema.table_constraints AS tc
|
|
143
|
-
JOIN information_schema.key_column_usage AS kcu
|
|
144
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
145
|
-
AND tc.table_schema = kcu.table_schema
|
|
146
|
-
JOIN information_schema.constraint_column_usage AS ccu
|
|
147
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
148
|
-
AND ccu.table_schema = tc.table_schema
|
|
149
|
-
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,
|
|
150
|
-
[pgSchema]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const tablesMap = buildTablesMap(tables, columns, pks, fks);
|
|
154
|
-
return {
|
|
155
|
-
tablesMap,
|
|
156
|
-
enumMap: buildEnumMap(enumValues),
|
|
157
|
-
joinTables: identifyJoinTables(tablesMap)
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Derive the `isId` flavour for a primary-key column. */
|
|
162
|
-
function idKindFor(col: TableColumn, propType: string): "uuid" | "increment" | true {
|
|
163
|
-
if (col.data_type.toLowerCase() === "uuid") return "uuid";
|
|
164
|
-
if (propType === "number") return "increment";
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function buildProperties(
|
|
169
|
-
meta: TableMeta,
|
|
170
|
-
enumMap: Map<string, string[]>
|
|
171
|
-
): Record<string, Record<string, unknown>> {
|
|
172
|
-
const properties: Record<string, Record<string, unknown>> = {};
|
|
173
|
-
const takenKeys = new Set<string>();
|
|
174
|
-
|
|
175
|
-
for (const col of meta.columns) {
|
|
176
|
-
const isPk = meta.pks.includes(col.column_name);
|
|
177
|
-
// Foreign keys surface as relations below, unless they are also part of
|
|
178
|
-
// the primary key, in which case the column itself must stay.
|
|
179
|
-
const isFk = meta.fks.some((fk) => fk.column_name === col.column_name);
|
|
180
|
-
if (isFk && !isPk) continue;
|
|
181
|
-
|
|
182
|
-
const enumValues = enumMap.get(col.udt_name);
|
|
183
|
-
const isEnum = col.data_type === "USER-DEFINED" && enumValues !== undefined;
|
|
184
|
-
const isVector = col.udt_name === "vector";
|
|
185
|
-
const propType = isEnum ? "string" : isVector ? "vector" : mapPgType(col.data_type);
|
|
186
|
-
|
|
187
|
-
const property: Record<string, unknown> = {
|
|
188
|
-
name: humanize(col.column_name),
|
|
189
|
-
columnName: col.column_name,
|
|
190
|
-
type: propType
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
// The wire name, with `columnName` above carrying the column. The two
|
|
194
|
-
// are different names for different things — this used to serve the
|
|
195
|
-
// column, so a runtime-introspected collection answered `user_id` while
|
|
196
|
-
// every authored one beside it answered `displayName`.
|
|
197
|
-
//
|
|
198
|
-
// `user_id` and `userId` as two real columns camel-case to one key, so
|
|
199
|
-
// the first free candidate wins and the second falls back to its own
|
|
200
|
-
// column name, then to a numbered tail. Never dropped, and stable for a
|
|
201
|
-
// given database: columns arrive in ordinal order.
|
|
202
|
-
const key = firstFreeKey([toWireKey(col.column_name), col.column_name], takenKeys);
|
|
203
|
-
takenKeys.add(key);
|
|
204
|
-
|
|
205
|
-
if (isPk) {
|
|
206
|
-
property.isId = idKindFor(col, propType);
|
|
207
|
-
} else if (col.is_nullable === "NO" && col.column_default === null) {
|
|
208
|
-
property.validation = { required: true };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (isEnum && enumValues) {
|
|
212
|
-
property.enum = enumValues.map((value) => ({ id: value, label: humanize(value) }));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
properties[key] = property;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return properties;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Owning relations, derived from this table's foreign keys — the same shape
|
|
223
|
-
* `generateCollectionFile` writes into a collection file: a `relation` property
|
|
224
|
-
* whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.
|
|
225
|
-
*
|
|
226
|
-
* The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads
|
|
227
|
-
* relations from `property.relation` and `resolveRelation` requires `target` to
|
|
228
|
-
* be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the
|
|
229
|
-
* property with the slug as a bare string, which satisfies neither. Nothing
|
|
230
|
-
* threw — the resolver simply skipped every such property and reported that the
|
|
231
|
-
* collection had no relations. So an introspected BaaS collection had its FK
|
|
232
|
-
* columns removed from `properties` (they "surface as relations") and then no
|
|
233
|
-
* resolvable relation to surface as, which is why writing the FK column
|
|
234
|
-
* directly came back as `has no field 'product_id'`: `assertKnownWriteFields`
|
|
235
|
-
* learns that column from the resolved relation's `localKey`.
|
|
236
|
-
*
|
|
237
|
-
* The thunk closes over the collections being built in this same pass rather
|
|
238
|
-
* than importing a module, which is what a runtime introspection has instead of
|
|
239
|
-
* generated files. It is called lazily, after the map is fully populated, so a
|
|
240
|
-
* table may reference one introspected later.
|
|
241
|
-
*/
|
|
242
|
-
function buildRelations(
|
|
243
|
-
meta: TableMeta,
|
|
244
|
-
slugByTable: Map<string, string>,
|
|
245
|
-
collectionBySlug: Map<string, PostgresCollectionConfig>
|
|
246
|
-
): Record<string, Record<string, unknown>> {
|
|
247
|
-
const relations: Record<string, Record<string, unknown>> = {};
|
|
248
|
-
|
|
249
|
-
for (const fk of meta.fks) {
|
|
250
|
-
const targetSlug = slugByTable.get(fk.foreign_table_name);
|
|
251
|
-
if (!targetSlug) continue;
|
|
252
|
-
|
|
253
|
-
// Strip the conventional _id suffix: author_id -> author
|
|
254
|
-
let key = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
255
|
-
if (meta.pks.includes(fk.column_name) && key === fk.column_name) {
|
|
256
|
-
// The FK is also the PK and its name doesn't imply a relation (e.g.
|
|
257
|
-
// "id"), so naming the relation after the column would collide with
|
|
258
|
-
// the primary-key property.
|
|
259
|
-
key = fk.foreign_table_name;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
relations[key] = {
|
|
263
|
-
name: humanize(key),
|
|
264
|
-
type: "relation",
|
|
265
|
-
relation: {
|
|
266
|
-
kind: "belongsTo",
|
|
267
|
-
target: () => collectionBySlug.get(targetSlug),
|
|
268
|
-
localKey: fk.column_name
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return relations;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Turn an introspected schema into collections.
|
|
278
|
-
*
|
|
279
|
-
* Join tables are skipped: they carry no identity of their own and exist to
|
|
280
|
-
* express a many-to-many edge between two other tables.
|
|
281
|
-
*/
|
|
282
|
-
export function buildCollectionsFromSchema(
|
|
283
|
-
{ tablesMap, enumMap, joinTables }: IntrospectedSchema,
|
|
284
|
-
pgSchema: string
|
|
285
|
-
): PostgresCollectionConfig[] {
|
|
286
|
-
const slugByTable = new Map<string, string>();
|
|
287
|
-
for (const tableName of tablesMap.keys()) {
|
|
288
|
-
if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const collections: PostgresCollectionConfig[] = [];
|
|
292
|
-
// Filled as we go; the relation thunks read it lazily, so a table may point
|
|
293
|
-
// at one that has not been built yet at the moment its relation is created.
|
|
294
|
-
const collectionBySlug = new Map<string, PostgresCollectionConfig>();
|
|
295
|
-
|
|
296
|
-
for (const [tableName, meta] of tablesMap) {
|
|
297
|
-
if (joinTables.has(tableName)) continue;
|
|
298
|
-
|
|
299
|
-
const collectionName = humanize(tableName);
|
|
300
|
-
const collection = {
|
|
301
|
-
name: collectionName,
|
|
302
|
-
singularName: singularize(collectionName),
|
|
303
|
-
slug: tableName,
|
|
304
|
-
table: tableName,
|
|
305
|
-
schema: pgSchema,
|
|
306
|
-
icon: getIconForTable(tableName),
|
|
307
|
-
properties: {
|
|
308
|
-
...buildProperties(meta, enumMap),
|
|
309
|
-
...buildRelations(meta, slugByTable, collectionBySlug)
|
|
310
|
-
}
|
|
311
|
-
} as unknown as PostgresCollectionConfig;
|
|
312
|
-
|
|
313
|
-
collections.push(collection);
|
|
314
|
-
collectionBySlug.set(tableName, collection);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return collections;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** Introspect the database and return ready-to-serve collections. */
|
|
321
|
-
export async function introspectCollections(
|
|
322
|
-
client: Queryable,
|
|
323
|
-
pgSchema: string
|
|
324
|
-
): Promise<PostgresCollectionConfig[]> {
|
|
325
|
-
const schema = await introspectSchema(client, pgSchema);
|
|
326
|
-
return buildCollectionsFromSchema(schema, pgSchema);
|
|
327
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A collection served by another engine is not this engine's to provision.
|
|
3
|
-
*
|
|
4
|
-
* Firestore and MongoDB collections are declared in the same
|
|
5
|
-
* `config/collections` directory as the Postgres ones — that is what
|
|
6
|
-
* `dataSource` routing is for — and every stage of the SQL toolchain used to
|
|
7
|
-
* read "the collections" as all of them. So a Firestore collection got a
|
|
8
|
-
* `pgTable` in the generated schema, a `CREATE TABLE` at boot, RLS policies,
|
|
9
|
-
* and an entry in the `db push` include list, while the app went on reading its
|
|
10
|
-
* documents from Firestore.
|
|
11
|
-
*
|
|
12
|
-
* The include list is the one that can lose data: a name on it is a name Atlas
|
|
13
|
-
* is *allowed to drop*, so a Firestore collection called `exercises` removed a
|
|
14
|
-
* real, unrelated `public.exercises` table's protection from the next
|
|
15
|
-
* auto-approved `db push`.
|
|
16
|
-
*/
|
|
17
|
-
import { describe, expect, it } from "@jest/globals";
|
|
18
|
-
import type { CollectionConfig } from "@rebasepro/types";
|
|
19
|
-
|
|
20
|
-
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
21
|
-
import {
|
|
22
|
-
generatePostgresDdl,
|
|
23
|
-
generatePostgresPoliciesDdl,
|
|
24
|
-
planCollectionPolicies
|
|
25
|
-
} from "./generate-postgres-ddl-logic";
|
|
26
|
-
import { planCollectionSchemaEnsure, type ExistingSchema } from "./ensure-collection-tables";
|
|
27
|
-
import { getTableIncludesFromCollections } from "../cli-helpers";
|
|
28
|
-
|
|
29
|
-
const sqlCollection = {
|
|
30
|
-
name: "Customers",
|
|
31
|
-
slug: "customers",
|
|
32
|
-
properties: {
|
|
33
|
-
id: { name: "ID", type: "string", isId: "uuid" },
|
|
34
|
-
email: { name: "Email", type: "string" }
|
|
35
|
-
},
|
|
36
|
-
securityRules: [{ operation: "read", policy: "true" }]
|
|
37
|
-
} as unknown as CollectionConfig;
|
|
38
|
-
|
|
39
|
-
const firestoreCollection = {
|
|
40
|
-
name: "Exercises",
|
|
41
|
-
slug: "exercises",
|
|
42
|
-
engine: "firestore",
|
|
43
|
-
dataSource: "firestore",
|
|
44
|
-
properties: {
|
|
45
|
-
id: { name: "ID", type: "string" },
|
|
46
|
-
title: { name: "Title", type: "string" },
|
|
47
|
-
difficulty: {
|
|
48
|
-
name: "Difficulty",
|
|
49
|
-
type: "string",
|
|
50
|
-
enum: [{ id: "easy", label: "Easy" }, { id: "hard", label: "Hard" }]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} as unknown as CollectionConfig;
|
|
54
|
-
|
|
55
|
-
const mongoCollection = {
|
|
56
|
-
name: "Events",
|
|
57
|
-
slug: "events",
|
|
58
|
-
engine: "mongodb",
|
|
59
|
-
properties: { id: { name: "ID", type: "string" } }
|
|
60
|
-
} as unknown as CollectionConfig;
|
|
61
|
-
|
|
62
|
-
/** Routed by `dataSource` alone — no `engine` field to read. */
|
|
63
|
-
const dataSourceOnlyCollection = {
|
|
64
|
-
name: "Articles",
|
|
65
|
-
slug: "articles",
|
|
66
|
-
dataSource: "firestore",
|
|
67
|
-
properties: { id: { name: "ID", type: "string" } }
|
|
68
|
-
} as unknown as CollectionConfig;
|
|
69
|
-
|
|
70
|
-
const all = [sqlCollection, firestoreCollection, mongoCollection, dataSourceOnlyCollection];
|
|
71
|
-
|
|
72
|
-
describe("the Drizzle schema generator", () => {
|
|
73
|
-
it("emits a table for the Postgres collection only", async () => {
|
|
74
|
-
const schema = await generateSchema(all);
|
|
75
|
-
|
|
76
|
-
expect(schema).toContain("customers");
|
|
77
|
-
expect(schema).not.toContain("exercises");
|
|
78
|
-
expect(schema).not.toContain("events");
|
|
79
|
-
expect(schema).not.toContain("articles");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("does not emit enum types for a document store's fields", async () => {
|
|
83
|
-
const schema = await generateSchema(all);
|
|
84
|
-
expect(schema).not.toContain("difficulty");
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("the Postgres DDL generator", () => {
|
|
89
|
-
it("creates no table for a non-SQL collection", async () => {
|
|
90
|
-
const ddl = await generatePostgresDdl(all, { includePolicies: false });
|
|
91
|
-
|
|
92
|
-
expect(ddl).toContain('CREATE TABLE "public"."customers"');
|
|
93
|
-
expect(ddl).not.toContain("exercises");
|
|
94
|
-
expect(ddl).not.toContain("events");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("plans no RLS for a store that has none", () => {
|
|
98
|
-
const policies = generatePostgresPoliciesDdl(all);
|
|
99
|
-
expect(policies).not.toContain("exercises");
|
|
100
|
-
|
|
101
|
-
const plans = planCollectionPolicies(all);
|
|
102
|
-
expect(plans.map(p => p.qualified)).toEqual(["public.customers"]);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("the boot-time schema ensure", () => {
|
|
107
|
-
const empty = (): ExistingSchema => ({ tables: new Map(), enums: new Set() });
|
|
108
|
-
|
|
109
|
-
it("creates nothing for collections another engine stores", () => {
|
|
110
|
-
const plan = planCollectionSchemaEnsure(all, empty());
|
|
111
|
-
|
|
112
|
-
expect(plan.actions.map(a => a.target)).toEqual(
|
|
113
|
-
expect.arrayContaining(["public.customers"])
|
|
114
|
-
);
|
|
115
|
-
for (const action of plan.actions) {
|
|
116
|
-
expect(action.target).not.toContain("exercises");
|
|
117
|
-
expect(action.target).not.toContain("events");
|
|
118
|
-
expect(action.target).not.toContain("articles");
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("the db push include list", () => {
|
|
124
|
-
it("does not claim a table for a Firestore collection", async () => {
|
|
125
|
-
// Anything absent from this list is *excluded* from the declarative
|
|
126
|
-
// apply, which is what protects it from being dropped.
|
|
127
|
-
const includes = await getTableIncludesFromCollections(all);
|
|
128
|
-
|
|
129
|
-
expect(includes).toEqual(["public.customers"]);
|
|
130
|
-
});
|
|
131
|
-
});
|