@rebasepro/server-postgres 0.25.0 → 0.25.1
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/{BranchService-iXXZeVYD.js → BranchService-ClklxyAQ.js} +66 -6
- package/dist/BranchService-ClklxyAQ.js.map +1 -0
- package/dist/backup/backup-cli.d.ts +40 -3
- package/dist/backup/backup-service.d.ts +11 -0
- package/dist/{backup-service-Bb39plam.js → backup-cli-gijeXe0u.js} +777 -10
- package/dist/backup-cli-gijeXe0u.js.map +1 -0
- package/dist/{boot-ddl-s_3Hkz94.js → boot-ddl-DYH95Qeg.js} +3 -3
- package/dist/{boot-ddl-s_3Hkz94.js.map → boot-ddl-DYH95Qeg.js.map} +1 -1
- package/dist/branch-argv.d.ts +15 -0
- package/dist/{cli-console-DQ_K-sdM.js → cli-console-CQrQ0lAY.js} +8 -9
- package/dist/{cli-console-DQ_K-sdM.js.map → cli-console-CQrQ0lAY.js.map} +1 -1
- package/dist/{cli-errors-DwQnNATs.js → cli-errors-DiA0XFIS.js} +38 -10
- package/dist/{cli-errors-DwQnNATs.js.map → cli-errors-DiA0XFIS.js.map} +1 -1
- package/dist/cli-errors.d.ts +14 -0
- package/dist/cli-flags.d.ts +51 -0
- package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
- package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
- package/dist/cli.js +44 -174
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
- package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
- package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
- package/dist/connection-czvI7TZL.js.map +1 -0
- package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
- package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
- package/dist/db-command-line.d.ts +14 -0
- package/dist/{rls-bootstrap-sql-Bd-hJk08.js → db-subcommands-BPoHCxJR.js} +455 -3
- package/dist/db-subcommands-BPoHCxJR.js.map +1 -0
- package/dist/db-subcommands.d.ts +11 -0
- package/dist/{deferred-index-build-Cis_HoGn.js → deferred-index-build-BE0I6sYU.js} +8 -8
- package/dist/{deferred-index-build-Cis_HoGn.js.map → deferred-index-build-BE0I6sYU.js.map} +1 -1
- package/dist/{doctor-DvJH-IQG.js → doctor-BT--1kq3.js} +107 -11
- package/dist/doctor-BT--1kq3.js.map +1 -0
- package/dist/{ensure-collection-policies-CCqayoUz.js → ensure-collection-policies-BnGOURpG.js} +13 -10
- package/dist/ensure-collection-policies-BnGOURpG.js.map +1 -0
- package/dist/{ensure-collection-tables-DvvImXDe.js → ensure-collection-tables-Cet4650m.js} +9 -9
- package/dist/{ensure-collection-tables-DvvImXDe.js.map → ensure-collection-tables-Cet4650m.js.map} +1 -1
- package/dist/{ensure-tables-Dpgwx5sy.js → ensure-tables-l_OgFQjN.js} +5 -5
- package/dist/{ensure-tables-Dpgwx5sy.js.map → ensure-tables-l_OgFQjN.js.map} +1 -1
- package/dist/{enum-labels-kept-CUlUlRkO.js → enum-labels-kept-nheku1pB.js} +3 -3
- package/dist/{enum-labels-kept-CUlUlRkO.js.map → enum-labels-kept-nheku1pB.js.map} +1 -1
- package/dist/{generate-drizzle-schema-C3EAP9HE.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
- package/dist/{generate-drizzle-schema-C3EAP9HE.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
- package/dist/{generate-files-ClmOUHXj.js → generate-files-BP-4W_gm.js} +6 -6
- package/dist/{generate-files-ClmOUHXj.js.map → generate-files-BP-4W_gm.js.map} +1 -1
- package/dist/{generate-next-step-DCeS4LrJ.js → generate-next-step-DgK5BlBn.js} +2 -2
- package/dist/{generate-next-step-DCeS4LrJ.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
- package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
- package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
- package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
- package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +70 -11
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-CI5CU_n0.js → introspect-db-logic-DgQVzccW.js} +2 -2
- package/dist/{introspect-db-logic-CI5CU_n0.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
- package/dist/migrate-policy-stamps-3eqfhes6.js +89 -0
- package/dist/migrate-policy-stamps-3eqfhes6.js.map +1 -0
- package/dist/{open-rebase-BKA7Nyoe.js → open-rebase-EmqVmS14.js} +51 -1020
- package/dist/open-rebase-EmqVmS14.js.map +1 -0
- package/dist/{pg-error-utils-Bv5n2PLn.js → pg-error-utils-CFEnm2m9.js} +2 -2
- package/dist/{pg-error-utils-Bv5n2PLn.js.map → pg-error-utils-CFEnm2m9.js.map} +1 -1
- package/dist/{plan-schema-CXOucCw-.js → plan-schema-Czf3q4-Q.js} +4 -4
- package/dist/{plan-schema-CXOucCw-.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
- package/dist/{policy-drift-vm0BcvcE.js → policy-drift-X4A-xAzv.js} +3 -3
- package/dist/{policy-drift-vm0BcvcE.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
- package/dist/pool-disconnects.d.ts +28 -0
- package/dist/{push-index-prebuild-DIequ4bV.js → push-index-prebuild-COd7i6wm.js} +4 -4
- package/dist/{push-index-prebuild-DIequ4bV.js.map → push-index-prebuild-COd7i6wm.js.map} +1 -1
- package/dist/{push-legacy-renames-BqJut4F3.js → push-legacy-renames-CLgYYwPE.js} +4 -4
- package/dist/{push-legacy-renames-BqJut4F3.js.map → push-legacy-renames-CLgYYwPE.js.map} +1 -1
- package/dist/{push-policy-plan-CvX8hiPb.js → push-policy-plan-D2gpQsdv.js} +5 -5
- package/dist/{push-policy-plan-CvX8hiPb.js.map → push-policy-plan-D2gpQsdv.js.map} +1 -1
- package/dist/{render-ddl-CQNwZdse.js → render-ddl-4v88y-Gv.js} +4 -4
- package/dist/{render-ddl-CQNwZdse.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
- package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-DDyNoOSy.js} +2 -2
- package/dist/{rls-enforcement-D7P4-ZYX.js.map → rls-enforcement-DDyNoOSy.js.map} +1 -1
- package/dist/schema/doctor-cli.js +5 -5
- package/dist/schema/doctor.d.ts +20 -2
- package/dist/schema/ensure-collection-policies.d.ts +4 -1
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +2 -2
- package/dist/schema/generated-schema-diff.d.ts +21 -0
- package/dist/schema/introspect-db.js +5 -6
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-policy-stamps.d.ts +23 -0
- package/dist/schema/push-policy-plan.d.ts +5 -0
- package/dist/services/BranchService.d.ts +20 -2
- package/dist/services/pg-notify-listener.d.ts +2 -0
- package/dist/{source-Bv5KPeIx.js → source-fZb92pN8.js} +2 -2
- package/dist/{source-Bv5KPeIx.js.map → source-fZb92pN8.js.map} +1 -1
- package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
- package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
- package/dist/{websocket-MC0zTBar.js → websocket-BKZwsjmf.js} +18 -5
- package/dist/websocket-BKZwsjmf.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-iXXZeVYD.js.map +0 -1
- package/dist/backup-cli-B2CAn9fR.js +0 -647
- package/dist/backup-cli-B2CAn9fR.js.map +0 -1
- package/dist/backup-service-Bb39plam.js.map +0 -1
- package/dist/cli-flags-D-b0qL9T.js +0 -206
- package/dist/cli-flags-D-b0qL9T.js.map +0 -1
- package/dist/connection-C938nR3E.js.map +0 -1
- package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
- package/dist/doctor-DvJH-IQG.js.map +0 -1
- package/dist/ensure-collection-policies-CCqayoUz.js.map +0 -1
- package/dist/open-rebase-BKA7Nyoe.js.map +0 -1
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
- package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
- package/dist/websocket-MC0zTBar.js.map +0 -1
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
__createRequire(import.meta.url);
|
|
3
|
+
import { l as searchColumnNames } from "./search-column-BM-GV6vH.js";
|
|
4
|
+
import { i as isAuthCollection, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-C72EMoDJ.js";
|
|
5
|
+
import { o as sharedRelationName } from "./plan-schema-Czf3q4-Q.js";
|
|
6
|
+
import { _ as humanize, g as mapPgType, i as buildTablesMap, l as getIconForTable, p as singularize, r as buildEnumMap, u as identifyJoinTables } from "./introspect-db-logic-DgQVzccW.js";
|
|
7
|
+
import { logger } from "@rebasepro/server";
|
|
8
|
+
import { getTableColumns, getTableName, isTable, relations, sql } from "drizzle-orm";
|
|
9
|
+
import { bigint, boolean, char, cidr, customType, date, doublePrecision, geometry, inet, integer, interval, json, jsonb, line, macaddr, macaddr8, numeric, pgSchema, pgTable, point, primaryKey, real, smallint, text, time, timestamp, uuid, varchar, vector } from "drizzle-orm/pg-core";
|
|
10
|
+
import { fieldKeyForColumn, getJunctionCollectionConfig, getTableName as getTableName$1, resolveCollectionRelations, resolveJunctionSpecs } from "@rebasepro/common";
|
|
11
|
+
import { firstFreeKey, toWireKey } from "@rebasepro/utils";
|
|
12
|
+
//#region src/schema/introspect-runtime.ts
|
|
13
|
+
/**
|
|
14
|
+
* Read the RLS posture of each table in a schema.
|
|
15
|
+
*
|
|
16
|
+
* This is what decides whether baas mode may serve a table. A table with RLS
|
|
17
|
+
* disabled has no authorization model: since every authenticated request runs
|
|
18
|
+
* as `rebase_user`, and that role is granted DML on the schema, serving such a
|
|
19
|
+
* table hands every row to every logged-in user.
|
|
20
|
+
*/
|
|
21
|
+
async function readRlsStatus(client, pgSchema) {
|
|
22
|
+
const { rows } = await client.query(`SELECT c.relname AS table,
|
|
23
|
+
c.relrowsecurity AS rls_enabled,
|
|
24
|
+
(SELECT count(*) FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count
|
|
25
|
+
FROM pg_class c
|
|
26
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
27
|
+
WHERE n.nspname = $1 AND c.relkind = 'r'`, [pgSchema]);
|
|
28
|
+
return new Map(rows.map((r) => [r.table, {
|
|
29
|
+
table: r.table,
|
|
30
|
+
rlsEnabled: r.rls_enabled === true,
|
|
31
|
+
policyCount: Number(r.policy_count ?? 0)
|
|
32
|
+
}]));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read tables, columns, enums, primary keys and foreign keys for a schema.
|
|
36
|
+
* Mirrors the queries in introspect-db.ts.
|
|
37
|
+
*/
|
|
38
|
+
async function introspectSchema(client, pgSchema) {
|
|
39
|
+
const { rows: tables } = await client.query(`SELECT table_name
|
|
40
|
+
FROM information_schema.tables
|
|
41
|
+
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
42
|
+
AND table_name NOT LIKE 'drizzle_%'
|
|
43
|
+
AND table_name NOT LIKE 'rebase_%'
|
|
44
|
+
ORDER BY table_name`, [pgSchema]);
|
|
45
|
+
const { rows: columns } = await client.query(`SELECT
|
|
46
|
+
c.table_name,
|
|
47
|
+
c.column_name,
|
|
48
|
+
c.data_type,
|
|
49
|
+
c.udt_name,
|
|
50
|
+
c.is_nullable,
|
|
51
|
+
c.column_default,
|
|
52
|
+
-- A stored generated column must be declared as one, or drizzle
|
|
53
|
+
-- writes DEFAULT into it on every INSERT. Postgres tolerates that
|
|
54
|
+
-- today; the generated module has always declared them properly,
|
|
55
|
+
-- and the runtime tables are built to match it.
|
|
56
|
+
c.is_generated,
|
|
57
|
+
c.generation_expression,
|
|
58
|
+
(SELECT a.atttypmod FROM pg_attribute a
|
|
59
|
+
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
60
|
+
WHERE pc.relname = c.table_name
|
|
61
|
+
AND a.attname = c.column_name
|
|
62
|
+
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
63
|
+
FROM information_schema.columns c
|
|
64
|
+
WHERE c.table_schema = $1`, [pgSchema]);
|
|
65
|
+
const { rows: enumValues } = await client.query(`SELECT t.typname AS enum_name,
|
|
66
|
+
e.enumlabel AS enum_value,
|
|
67
|
+
e.enumsortorder AS sort_order
|
|
68
|
+
FROM pg_type t
|
|
69
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
70
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
71
|
+
WHERE n.nspname = $1
|
|
72
|
+
ORDER BY t.typname, e.enumsortorder`, [pgSchema]);
|
|
73
|
+
const { rows: pks } = await client.query(`SELECT t.relname as table_name, a.attname as column_name
|
|
74
|
+
FROM pg_index i
|
|
75
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
76
|
+
AND a.attnum = ANY(i.indkey)
|
|
77
|
+
JOIN pg_class t ON t.oid = i.indrelid
|
|
78
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
79
|
+
WHERE i.indisprimary AND n.nspname = $1`, [pgSchema]);
|
|
80
|
+
const { rows: fks } = await client.query(`SELECT
|
|
81
|
+
tc.table_name,
|
|
82
|
+
kcu.column_name,
|
|
83
|
+
ccu.table_name AS foreign_table_name,
|
|
84
|
+
ccu.column_name AS foreign_column_name
|
|
85
|
+
FROM information_schema.table_constraints AS tc
|
|
86
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
87
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
88
|
+
AND tc.table_schema = kcu.table_schema
|
|
89
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
90
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
91
|
+
AND ccu.table_schema = tc.table_schema
|
|
92
|
+
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`, [pgSchema]);
|
|
93
|
+
const tablesMap = buildTablesMap(tables, columns, pks, fks);
|
|
94
|
+
return {
|
|
95
|
+
tablesMap,
|
|
96
|
+
enumMap: buildEnumMap(enumValues),
|
|
97
|
+
joinTables: identifyJoinTables(tablesMap)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Derive the `isId` flavour for a primary-key column. */
|
|
101
|
+
function idKindFor(col, propType) {
|
|
102
|
+
if (col.data_type.toLowerCase() === "uuid") return "uuid";
|
|
103
|
+
if (propType === "number") return "increment";
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function buildProperties(meta, enumMap) {
|
|
107
|
+
const properties = {};
|
|
108
|
+
const takenKeys = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const col of meta.columns) {
|
|
110
|
+
const isPk = meta.pks.includes(col.column_name);
|
|
111
|
+
if (meta.fks.some((fk) => fk.column_name === col.column_name) && !isPk) continue;
|
|
112
|
+
const enumValues = enumMap.get(col.udt_name);
|
|
113
|
+
const isEnum = col.data_type === "USER-DEFINED" && enumValues !== void 0;
|
|
114
|
+
const isVector = col.udt_name === "vector";
|
|
115
|
+
const propType = isEnum ? "string" : isVector ? "vector" : mapPgType(col.data_type);
|
|
116
|
+
const property = {
|
|
117
|
+
name: humanize(col.column_name),
|
|
118
|
+
columnName: col.column_name,
|
|
119
|
+
type: propType
|
|
120
|
+
};
|
|
121
|
+
const key = firstFreeKey([toWireKey(col.column_name), col.column_name], takenKeys);
|
|
122
|
+
takenKeys.add(key);
|
|
123
|
+
if (isPk) property.isId = idKindFor(col, propType);
|
|
124
|
+
else if (col.is_nullable === "NO" && col.column_default === null) property.validation = { required: true };
|
|
125
|
+
if (isEnum && enumValues) property.enum = enumValues.map((value) => ({
|
|
126
|
+
id: value,
|
|
127
|
+
label: humanize(value)
|
|
128
|
+
}));
|
|
129
|
+
properties[key] = property;
|
|
130
|
+
}
|
|
131
|
+
return properties;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Owning relations, derived from this table's foreign keys — the same shape
|
|
135
|
+
* `generateCollectionFile` writes into a collection file: a `relation` property
|
|
136
|
+
* whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.
|
|
137
|
+
*
|
|
138
|
+
* The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads
|
|
139
|
+
* relations from `property.relation` and `resolveRelation` requires `target` to
|
|
140
|
+
* be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the
|
|
141
|
+
* property with the slug as a bare string, which satisfies neither. Nothing
|
|
142
|
+
* threw — the resolver simply skipped every such property and reported that the
|
|
143
|
+
* collection had no relations. So an introspected BaaS collection had its FK
|
|
144
|
+
* columns removed from `properties` (they "surface as relations") and then no
|
|
145
|
+
* resolvable relation to surface as, which is why writing the FK column
|
|
146
|
+
* directly came back as `has no field 'product_id'`: `assertKnownWriteFields`
|
|
147
|
+
* learns that column from the resolved relation's `localKey`.
|
|
148
|
+
*
|
|
149
|
+
* The thunk closes over the collections being built in this same pass rather
|
|
150
|
+
* than importing a module, which is what a runtime introspection has instead of
|
|
151
|
+
* generated files. It is called lazily, after the map is fully populated, so a
|
|
152
|
+
* table may reference one introspected later.
|
|
153
|
+
*/
|
|
154
|
+
function buildRelations(meta, slugByTable, collectionBySlug) {
|
|
155
|
+
const relations = {};
|
|
156
|
+
for (const fk of meta.fks) {
|
|
157
|
+
const targetSlug = slugByTable.get(fk.foreign_table_name);
|
|
158
|
+
if (!targetSlug) continue;
|
|
159
|
+
let key = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
160
|
+
if (meta.pks.includes(fk.column_name) && key === fk.column_name) key = fk.foreign_table_name;
|
|
161
|
+
relations[key] = {
|
|
162
|
+
name: humanize(key),
|
|
163
|
+
type: "relation",
|
|
164
|
+
relation: {
|
|
165
|
+
kind: "belongsTo",
|
|
166
|
+
target: () => collectionBySlug.get(targetSlug),
|
|
167
|
+
localKey: fk.column_name
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return relations;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Turn an introspected schema into collections.
|
|
175
|
+
*
|
|
176
|
+
* Join tables are skipped: they carry no identity of their own and exist to
|
|
177
|
+
* express a many-to-many edge between two other tables.
|
|
178
|
+
*/
|
|
179
|
+
function buildCollectionsFromSchema({ tablesMap, enumMap, joinTables }, pgSchema) {
|
|
180
|
+
const slugByTable = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const tableName of tablesMap.keys()) if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);
|
|
182
|
+
const collections = [];
|
|
183
|
+
const collectionBySlug = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const [tableName, meta] of tablesMap) {
|
|
185
|
+
if (joinTables.has(tableName)) continue;
|
|
186
|
+
const collectionName = humanize(tableName);
|
|
187
|
+
const collection = {
|
|
188
|
+
name: collectionName,
|
|
189
|
+
singularName: singularize(collectionName),
|
|
190
|
+
slug: tableName,
|
|
191
|
+
table: tableName,
|
|
192
|
+
schema: pgSchema,
|
|
193
|
+
icon: getIconForTable(tableName),
|
|
194
|
+
properties: {
|
|
195
|
+
...buildProperties(meta, enumMap),
|
|
196
|
+
...buildRelations(meta, slugByTable, collectionBySlug)
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
collections.push(collection);
|
|
200
|
+
collectionBySlug.set(tableName, collection);
|
|
201
|
+
}
|
|
202
|
+
return collections;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/schema/dynamic-tables.ts
|
|
206
|
+
/**
|
|
207
|
+
* Build drizzle tables at runtime from an introspected schema.
|
|
208
|
+
*
|
|
209
|
+
* **Every** data source's tables are built here, from `information_schema`,
|
|
210
|
+
* after boot-ensure has run — a project with declared collections included. The
|
|
211
|
+
* committed `schema.generated.ts` is a build artefact for Atlas, `db push`,
|
|
212
|
+
* `eject` and user code; the runtime does not read it for its tables, because a
|
|
213
|
+
* file that is one commit behind the database used to change what the server
|
|
214
|
+
* served (a renamed foreign-key column 500'd every request, a lost relation
|
|
215
|
+
* silently emptied a list). See `catalogue-schema.ts` for the caller.
|
|
216
|
+
*
|
|
217
|
+
* These are handed to drizzle as its schema, which keeps the relational query
|
|
218
|
+
* path (`db.query.*`) working; without them FetchService would fall back to
|
|
219
|
+
* plain selects and lose relation loading.
|
|
220
|
+
*
|
|
221
|
+
* Two things a caller has to supply for the tables to match what the generated
|
|
222
|
+
* module used to produce, both about *names*: the Drizzle object is keyed by the
|
|
223
|
+
* wire name of a column and not by the column (`authorId`, not `author_id`), and
|
|
224
|
+
* a relation is keyed by the collection's own relation key rather than by a
|
|
225
|
+
* guess made from the foreign key. Both come from the collections — see
|
|
226
|
+
* `columnKeysFromCollections` and `buildDrizzleRelationsFromCollections`.
|
|
227
|
+
*/
|
|
228
|
+
var columnNameAsKey = (_table, column) => column;
|
|
229
|
+
/** drizzle ships no bytea builder; binary must round-trip as a Buffer. */
|
|
230
|
+
var bytea = customType({ dataType: () => "bytea" });
|
|
231
|
+
/**
|
|
232
|
+
* A full-text search column, declared with its real SQL type.
|
|
233
|
+
*
|
|
234
|
+
* Not cosmetic: `isSearchIndexColumn` asks the column for its type and drops
|
|
235
|
+
* every `tsvector` from the projection, so a search column that fell through to
|
|
236
|
+
* `text` (the old default for an unrecognised type) would be selected, returned
|
|
237
|
+
* to callers, and rendered in the admin as a wall of lexemes. drizzle ships no
|
|
238
|
+
* builder for these two.
|
|
239
|
+
*/
|
|
240
|
+
var tsvector = customType({ dataType: () => "tsvector" });
|
|
241
|
+
var tsquery = customType({ dataType: () => "tsquery" });
|
|
242
|
+
/**
|
|
243
|
+
* Postgres stores a column's type modifier (varchar length, vector dimensions)
|
|
244
|
+
* in `atttypmod`. For length-carrying string types it is length + VARHDRSZ(4);
|
|
245
|
+
* for pgvector it is the dimension count as-is. -1 means unspecified.
|
|
246
|
+
*/
|
|
247
|
+
function varlenLength(col) {
|
|
248
|
+
return col.atttypmod && col.atttypmod > 4 ? col.atttypmod - 4 : void 0;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Map a Postgres type to a drizzle column builder, keyed on `udt_name` — the
|
|
252
|
+
* concrete underlying type, which is exact where `data_type` reports umbrella
|
|
253
|
+
* values like "ARRAY" or "USER-DEFINED".
|
|
254
|
+
*
|
|
255
|
+
* Unknown types fall back to `text`: the driver still reads and writes them,
|
|
256
|
+
* with the value passing through as a string, which beats dropping the column.
|
|
257
|
+
*/
|
|
258
|
+
function scalarBuilder(udtName, name, col) {
|
|
259
|
+
switch (udtName) {
|
|
260
|
+
case "uuid": return uuid(name);
|
|
261
|
+
case "bool": return boolean(name);
|
|
262
|
+
case "int2": return smallint(name);
|
|
263
|
+
case "int4": return integer(name);
|
|
264
|
+
case "int8": return bigint(name, { mode: "number" });
|
|
265
|
+
case "float4": return real(name);
|
|
266
|
+
case "float8": return doublePrecision(name);
|
|
267
|
+
case "numeric":
|
|
268
|
+
case "money": return numeric(name);
|
|
269
|
+
case "json": return json(name);
|
|
270
|
+
case "jsonb": return jsonb(name);
|
|
271
|
+
case "date": return date(name);
|
|
272
|
+
case "time": return time(name);
|
|
273
|
+
case "timetz": return time(name, { withTimezone: true });
|
|
274
|
+
case "timestamp": return timestamp(name, { mode: "string" });
|
|
275
|
+
case "timestamptz": return timestamp(name, {
|
|
276
|
+
withTimezone: true,
|
|
277
|
+
mode: "string"
|
|
278
|
+
});
|
|
279
|
+
case "interval": return interval(name);
|
|
280
|
+
case "bytea": return bytea(name);
|
|
281
|
+
case "inet": return inet(name);
|
|
282
|
+
case "cidr": return cidr(name);
|
|
283
|
+
case "macaddr": return macaddr(name);
|
|
284
|
+
case "macaddr8": return macaddr8(name);
|
|
285
|
+
case "point": return point(name);
|
|
286
|
+
case "line": return line(name);
|
|
287
|
+
case "geometry": return geometry(name);
|
|
288
|
+
case "vector": {
|
|
289
|
+
const dimensions = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : void 0;
|
|
290
|
+
return dimensions ? vector(name, { dimensions }) : text(name);
|
|
291
|
+
}
|
|
292
|
+
case "bpchar": {
|
|
293
|
+
const length = varlenLength(col);
|
|
294
|
+
return length ? char(name, { length }) : char(name);
|
|
295
|
+
}
|
|
296
|
+
case "varchar": {
|
|
297
|
+
const length = varlenLength(col);
|
|
298
|
+
return length ? varchar(name, { length }) : text(name);
|
|
299
|
+
}
|
|
300
|
+
case "tsvector": return tsvector(name);
|
|
301
|
+
case "tsquery": return tsquery(name);
|
|
302
|
+
default: return text(name);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** See {@link FluentColumnBuilder}. */
|
|
306
|
+
var fluent = (builder) => builder;
|
|
307
|
+
function columnBuilderFor(col) {
|
|
308
|
+
if (col.udt_name.startsWith("_")) return fluent(scalarBuilder(col.udt_name.slice(1), col.column_name, col)).array();
|
|
309
|
+
return scalarBuilder(col.udt_name, col.column_name, col);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Build one drizzle table per introspected table, keyed by table name.
|
|
313
|
+
*
|
|
314
|
+
* @param columnKey how a column is named on the wire — see {@link ColumnKeyResolver}.
|
|
315
|
+
*/
|
|
316
|
+
function buildDrizzleTablesFromSchema(tablesMap, pgSchemaName = "public", columnKey = columnNameAsKey) {
|
|
317
|
+
const schema = pgSchemaName === "public" ? null : pgSchema(pgSchemaName);
|
|
318
|
+
const createTable = schema ? schema.table.bind(schema) : pgTable;
|
|
319
|
+
const tables = {};
|
|
320
|
+
for (const [tableName, meta] of tablesMap) {
|
|
321
|
+
const columns = {};
|
|
322
|
+
const keyOf = /* @__PURE__ */ new Map();
|
|
323
|
+
for (const col of meta.columns) {
|
|
324
|
+
let builder = columnBuilderFor(col);
|
|
325
|
+
if (col.is_nullable === "NO") builder = fluent(builder).notNull();
|
|
326
|
+
if (meta.pks.length === 1 && meta.pks[0] === col.column_name) builder = fluent(builder).primaryKey();
|
|
327
|
+
if (col.is_generated === "ALWAYS") builder = fluent(builder).generatedAlwaysAs(sql.raw(col.generation_expression ?? ""));
|
|
328
|
+
const key = columnKey(tableName, col.column_name);
|
|
329
|
+
keyOf.set(col.column_name, key);
|
|
330
|
+
columns[key] = builder;
|
|
331
|
+
}
|
|
332
|
+
tables[tableName] = createTable(tableName, columns, meta.pks.length > 1 ? (t) => [primaryKey({ columns: meta.pks.map((pk) => t[keyOf.get(pk) ?? pk]) })] : void 0);
|
|
333
|
+
}
|
|
334
|
+
return tables;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Build drizzle `relations()` for the foreign keys, so the relational query
|
|
338
|
+
* path can actually load them.
|
|
339
|
+
*
|
|
340
|
+
* FetchService asks drizzle for `with: { <key>: true }`, keyed by the relation
|
|
341
|
+
* property on the collection. Tables alone don't satisfy that — without these,
|
|
342
|
+
* `?include=author` silently returns the raw `author_id` and no author.
|
|
343
|
+
*
|
|
344
|
+
* The keys here must match `buildRelations` in introspect-runtime.ts, which is
|
|
345
|
+
* what names the collection's relation properties.
|
|
346
|
+
*/
|
|
347
|
+
function buildDrizzleRelationsFromSchema(tablesMap, tables, columnKey = columnNameAsKey) {
|
|
348
|
+
/** Owning side, per table: the `one()` relations from its foreign keys. */
|
|
349
|
+
const owning = /* @__PURE__ */ new Map();
|
|
350
|
+
/** Inverse side, per referenced table: the matching `many()` back-references. */
|
|
351
|
+
const inverse = /* @__PURE__ */ new Map();
|
|
352
|
+
for (const [tableName, meta] of tablesMap) {
|
|
353
|
+
if (!tables[tableName]) continue;
|
|
354
|
+
const columnNames = new Set(meta.columns.map((c) => columnKey(tableName, c.column_name)));
|
|
355
|
+
for (const fk of meta.fks) {
|
|
356
|
+
if (!tables[fk.foreign_table_name]) continue;
|
|
357
|
+
let key = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
358
|
+
if (meta.pks.includes(fk.column_name) && key === fk.column_name) key = fk.foreign_table_name;
|
|
359
|
+
if (columnNames.has(key)) continue;
|
|
360
|
+
const relationName = `${tableName}_${fk.column_name}`;
|
|
361
|
+
owning.set(tableName, [...owning.get(tableName) ?? [], {
|
|
362
|
+
key,
|
|
363
|
+
targetTable: fk.foreign_table_name,
|
|
364
|
+
fkColumn: columnKey(tableName, fk.column_name),
|
|
365
|
+
targetColumn: columnKey(fk.foreign_table_name, fk.foreign_column_name),
|
|
366
|
+
relationName
|
|
367
|
+
}]);
|
|
368
|
+
const backKey = tableName;
|
|
369
|
+
if (new Set((tablesMap.get(fk.foreign_table_name)?.columns ?? []).map((c) => columnKey(fk.foreign_table_name, c.column_name))).has(backKey)) continue;
|
|
370
|
+
inverse.set(fk.foreign_table_name, [...inverse.get(fk.foreign_table_name) ?? [], {
|
|
371
|
+
key: backKey,
|
|
372
|
+
sourceTable: tableName,
|
|
373
|
+
relationName
|
|
374
|
+
}]);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const built = {};
|
|
378
|
+
for (const tableName of tablesMap.keys()) {
|
|
379
|
+
const table = tables[tableName];
|
|
380
|
+
const ones = owning.get(tableName) ?? [];
|
|
381
|
+
const manys = inverse.get(tableName) ?? [];
|
|
382
|
+
if (!table || ones.length === 0 && manys.length === 0) continue;
|
|
383
|
+
built[`${tableName}Relations`] = relations(table, ({ one, many }) => {
|
|
384
|
+
const map = {};
|
|
385
|
+
for (const rel of ones) map[rel.key] = one(tables[rel.targetTable], {
|
|
386
|
+
fields: [getTableColumns(table)[rel.fkColumn]],
|
|
387
|
+
references: [getTableColumns(tables[rel.targetTable])[rel.targetColumn]],
|
|
388
|
+
relationName: rel.relationName
|
|
389
|
+
});
|
|
390
|
+
for (const rel of manys) {
|
|
391
|
+
if (map[rel.key]) continue;
|
|
392
|
+
map[rel.key] = many(tables[rel.sourceTable], { relationName: rel.relationName });
|
|
393
|
+
}
|
|
394
|
+
return map;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return built;
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/schema/config-relations.ts
|
|
401
|
+
/**
|
|
402
|
+
* Build drizzle `relations()` for declared collections, from the collections.
|
|
403
|
+
*
|
|
404
|
+
* The heuristic builder in `dynamic-tables.ts` names a relation after the
|
|
405
|
+
* foreign-key column (`author_id` → `author`), which is the only evidence a
|
|
406
|
+
* database offers and is right for BaaS mode. It is wrong for a project that
|
|
407
|
+
* declares its collections: there the relation's name is whatever the author
|
|
408
|
+
* called it, `include=writer` addresses that name, and a relation whose key was
|
|
409
|
+
* guessed from the column simply does not answer to it — the read returns the
|
|
410
|
+
* bare foreign key and no target, silently, which is how a demo lost every
|
|
411
|
+
* relation in its list view while the detail view (a different code path) kept
|
|
412
|
+
* working.
|
|
413
|
+
*
|
|
414
|
+
* So when there are collections, they are the authority for *which* relations
|
|
415
|
+
* exist and what they are called; the live catalogue is the authority for the
|
|
416
|
+
* columns underneath. This is the same pairing `generate-drizzle-schema-logic`
|
|
417
|
+
* emits into `schema.generated.ts` — kind for kind, key for key, and paired by
|
|
418
|
+
* the same {@link sharedRelationName} — only built against tables read back from
|
|
419
|
+
* `information_schema` rather than against a file.
|
|
420
|
+
*/
|
|
421
|
+
/** A collection's table, with any schema prefix stripped — as the DDL creates it. */
|
|
422
|
+
function bareTableName(name) {
|
|
423
|
+
return name.includes(".") ? name.split(".").pop() : name;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The field key a collection's rows are addressed by, as the generated module
|
|
427
|
+
* spells it: the property marked `isId`, else `id`.
|
|
428
|
+
*
|
|
429
|
+
* Deliberately the config's answer rather than the catalogue's primary key —
|
|
430
|
+
* `references: [users.id]` in the generated file is keyed the same way, and the
|
|
431
|
+
* two have to name the same Drizzle key for a relation to compile at all.
|
|
432
|
+
*/
|
|
433
|
+
function primaryKeyFieldKey(collection) {
|
|
434
|
+
for (const [key, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && Boolean(prop.isId)) return key;
|
|
435
|
+
return "id";
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* The field key on the source table that a `hasOne`/`hasMany` foreign key
|
|
439
|
+
* holds: its `sourceKey` when the link names one, else the primary key.
|
|
440
|
+
*/
|
|
441
|
+
function sourceKeyFieldKey(relation, collection) {
|
|
442
|
+
return relation.sourceKey ? fieldKeyForColumn(collection, relation.sourceKey) : primaryKeyFieldKey(collection);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* A column of a built table, by its Drizzle key or by its column name.
|
|
446
|
+
*
|
|
447
|
+
* The key is the answer in the ordinary case, and the name is the fallback for
|
|
448
|
+
* the one place a caller holds a column and not a field: a junction table's two
|
|
449
|
+
* foreign keys, which are keyed by column *unless* the project also declares a
|
|
450
|
+
* collection over that table, in which case they are keyed by its property
|
|
451
|
+
* names. Looking both up costs a scan of one table and removes the difference.
|
|
452
|
+
*/
|
|
453
|
+
function columnOf(table, key) {
|
|
454
|
+
if (!table) return void 0;
|
|
455
|
+
const direct = getTableColumns(table)[key];
|
|
456
|
+
if (direct) return direct;
|
|
457
|
+
for (const column of Object.values(getTableColumns(table))) if (column?.name === key) return column;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Drizzle relations for every declared collection, keyed `<table>Relations`.
|
|
461
|
+
*
|
|
462
|
+
* @param collections every collection backed by a table in `tables`
|
|
463
|
+
* @param tables catalogue-built tables, keyed by bare table name
|
|
464
|
+
*/
|
|
465
|
+
function buildDrizzleRelationsFromCollections(collections, tables) {
|
|
466
|
+
const ones = /* @__PURE__ */ new Map();
|
|
467
|
+
const manys = /* @__PURE__ */ new Map();
|
|
468
|
+
/** `<relationName>::<kind>` already emitted on a table — the generator dedupes the same way. */
|
|
469
|
+
const emitted = /* @__PURE__ */ new Map();
|
|
470
|
+
const claim = (table, name, kind) => {
|
|
471
|
+
const seen = emitted.get(table) ?? /* @__PURE__ */ new Set();
|
|
472
|
+
emitted.set(table, seen);
|
|
473
|
+
const key = `${name}::${kind}`;
|
|
474
|
+
if (seen.has(key)) return false;
|
|
475
|
+
seen.add(key);
|
|
476
|
+
return true;
|
|
477
|
+
};
|
|
478
|
+
const addOne = (table, spec) => ones.set(table, [...ones.get(table) ?? [], spec]);
|
|
479
|
+
const addMany = (table, spec) => manys.set(table, [...manys.get(table) ?? [], spec]);
|
|
480
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const collection of collections) {
|
|
482
|
+
const table = bareTableName(getTableName$1(collection));
|
|
483
|
+
if (table && tables[table]) byTable.set(table, collection);
|
|
484
|
+
}
|
|
485
|
+
for (const [tableName, collection] of byTable) {
|
|
486
|
+
const table = tables[tableName];
|
|
487
|
+
for (const [relationKey, relation] of Object.entries(resolveCollectionRelations(collection))) {
|
|
488
|
+
let target;
|
|
489
|
+
try {
|
|
490
|
+
target = relation.target();
|
|
491
|
+
} catch (err) {
|
|
492
|
+
logger.debug(`[schema] Relation ${collection.slug}.${relationKey} has no resolvable target`, { error: err });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const targetTable = tables[bareTableName(getTableName$1(target))];
|
|
496
|
+
const name = sharedRelationName(relation, collection);
|
|
497
|
+
switch (relation.kind) {
|
|
498
|
+
case "belongsTo": {
|
|
499
|
+
if (!targetTable) continue;
|
|
500
|
+
if (!claim(tableName, name, relation.kind)) continue;
|
|
501
|
+
const localKey = fieldKeyForColumn(collection, relation.localKey);
|
|
502
|
+
const referenced = primaryKeyFieldKey(target);
|
|
503
|
+
const field = columnOf(table, localKey);
|
|
504
|
+
const reference = columnOf(targetTable, referenced);
|
|
505
|
+
if (!field || !reference) continue;
|
|
506
|
+
addOne(tableName, {
|
|
507
|
+
key: relationKey,
|
|
508
|
+
target: targetTable,
|
|
509
|
+
fields: [field],
|
|
510
|
+
references: [reference],
|
|
511
|
+
relationName: name
|
|
512
|
+
});
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case "hasOne": {
|
|
516
|
+
if (!targetTable) continue;
|
|
517
|
+
if (!claim(tableName, name, relation.kind)) continue;
|
|
518
|
+
const field = columnOf(table, sourceKeyFieldKey(relation, collection));
|
|
519
|
+
const reference = columnOf(targetTable, fieldKeyForColumn(target, relation.foreignKeyOnTarget));
|
|
520
|
+
if (!field || !reference) continue;
|
|
521
|
+
addOne(tableName, {
|
|
522
|
+
key: relationKey,
|
|
523
|
+
target: targetTable,
|
|
524
|
+
fields: [field],
|
|
525
|
+
references: [reference],
|
|
526
|
+
relationName: name
|
|
527
|
+
});
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
case "hasMany":
|
|
531
|
+
if (!targetTable) continue;
|
|
532
|
+
if (!claim(tableName, name, relation.kind)) continue;
|
|
533
|
+
addMany(tableName, {
|
|
534
|
+
key: relationKey,
|
|
535
|
+
target: targetTable,
|
|
536
|
+
relationName: name
|
|
537
|
+
});
|
|
538
|
+
break;
|
|
539
|
+
case "manyToMany": {
|
|
540
|
+
const junctionName = bareTableName(relation.through.table);
|
|
541
|
+
const junction = tables[junctionName];
|
|
542
|
+
if (!junction) continue;
|
|
543
|
+
if (!claim(tableName, name, relation.kind)) continue;
|
|
544
|
+
addMany(tableName, {
|
|
545
|
+
key: relationKey,
|
|
546
|
+
target: junction,
|
|
547
|
+
relationName: name
|
|
548
|
+
});
|
|
549
|
+
addJunctionSides(relation, collection, target, junctionName, tables, addOne, claim);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case "via": break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const [tableName, collection] of byTable) {
|
|
557
|
+
const table = tables[tableName];
|
|
558
|
+
for (const [, relation] of Object.entries(resolveCollectionRelations(collection))) {
|
|
559
|
+
if (relation.kind !== "hasMany" && relation.kind !== "hasOne") continue;
|
|
560
|
+
let target;
|
|
561
|
+
try {
|
|
562
|
+
target = relation.target();
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const targetTableName = bareTableName(getTableName$1(target));
|
|
567
|
+
const targetTable = tables[targetTableName];
|
|
568
|
+
if (!targetTable) continue;
|
|
569
|
+
const name = sharedRelationName(relation, collection);
|
|
570
|
+
if (!claim(targetTableName, name, "belongsTo")) continue;
|
|
571
|
+
const fkKey = fieldKeyForColumn(target, relation.foreignKeyOnTarget);
|
|
572
|
+
const field = columnOf(targetTable, fkKey);
|
|
573
|
+
const reference = columnOf(table, sourceKeyFieldKey(relation, collection));
|
|
574
|
+
if (!field || !reference) continue;
|
|
575
|
+
addOne(targetTableName, {
|
|
576
|
+
key: `_synth_${targetTableName}_${fkKey}`,
|
|
577
|
+
target: tables[tableName],
|
|
578
|
+
fields: [field],
|
|
579
|
+
references: [reference],
|
|
580
|
+
relationName: name
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const built = {};
|
|
585
|
+
for (const tableName of /* @__PURE__ */ new Set([...ones.keys(), ...manys.keys()])) {
|
|
586
|
+
const table = tables[tableName];
|
|
587
|
+
if (!table) continue;
|
|
588
|
+
const tableOnes = ones.get(tableName) ?? [];
|
|
589
|
+
const tableManys = manys.get(tableName) ?? [];
|
|
590
|
+
built[`${tableName}Relations`] = relations(table, ({ one, many }) => {
|
|
591
|
+
const map = {};
|
|
592
|
+
for (const spec of tableOnes) map[spec.key] = one(spec.target, {
|
|
593
|
+
fields: spec.fields,
|
|
594
|
+
references: spec.references,
|
|
595
|
+
relationName: spec.relationName
|
|
596
|
+
});
|
|
597
|
+
for (const spec of tableManys) {
|
|
598
|
+
if (map[spec.key]) continue;
|
|
599
|
+
map[spec.key] = many(spec.target, { relationName: spec.relationName });
|
|
600
|
+
}
|
|
601
|
+
return map;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return built;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* The junction table's own two `one()` sides.
|
|
608
|
+
*
|
|
609
|
+
* A many-to-many is a `many(junction)` on each endpoint, so the junction has to
|
|
610
|
+
* carry the matching `one()` back to each — without them drizzle can join a
|
|
611
|
+
* post to its `posts_tags` rows and no further, and the tags come back as
|
|
612
|
+
* junction rows. Keyed by the junction's own columns, which is how the
|
|
613
|
+
* generated file keys them too.
|
|
614
|
+
*/
|
|
615
|
+
function addJunctionSides(relation, sourceCollection, targetCollection, junctionName, tables, addOne, claim) {
|
|
616
|
+
const junction = tables[junctionName];
|
|
617
|
+
const sourceTable = tables[bareTableName(getTableName$1(sourceCollection))];
|
|
618
|
+
const targetTable = tables[bareTableName(getTableName$1(targetCollection))];
|
|
619
|
+
if (!junction || !sourceTable || !targetTable) return;
|
|
620
|
+
const sourceName = sharedRelationName(relation, sourceCollection);
|
|
621
|
+
const inverse = Object.values(resolveCollectionRelations(targetCollection)).find((r) => r.kind === "manyToMany" && bareTableName(r.through.table) === junctionName);
|
|
622
|
+
const targetName = inverse ? sharedRelationName(inverse, targetCollection) : `${junctionName}_${relation.through.targetColumn}`;
|
|
623
|
+
const pairs = [[
|
|
624
|
+
relation.through.sourceColumn,
|
|
625
|
+
sourceTable,
|
|
626
|
+
sourceName,
|
|
627
|
+
sourceCollection,
|
|
628
|
+
"source"
|
|
629
|
+
], [
|
|
630
|
+
relation.through.targetColumn,
|
|
631
|
+
targetTable,
|
|
632
|
+
targetName,
|
|
633
|
+
targetCollection,
|
|
634
|
+
"target"
|
|
635
|
+
]];
|
|
636
|
+
for (const [column, endpointTable, relationName, endpointCollection] of pairs) {
|
|
637
|
+
if (!claim(junctionName, relationName, "junction")) continue;
|
|
638
|
+
const field = columnOf(junction, column);
|
|
639
|
+
const reference = columnOf(endpointTable, primaryKeyFieldKey(endpointCollection));
|
|
640
|
+
if (!field || !reference) continue;
|
|
641
|
+
addOne(junctionName, {
|
|
642
|
+
key: column,
|
|
643
|
+
target: endpointTable,
|
|
644
|
+
fields: [field],
|
|
645
|
+
references: [reference],
|
|
646
|
+
relationName
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/schema/catalogue-schema.ts
|
|
652
|
+
/** The Postgres schema a collection's table lives in. */
|
|
653
|
+
function schemaOfCollection(collection, fallback) {
|
|
654
|
+
const declared = collection.schema;
|
|
655
|
+
return typeof declared === "string" && declared.length > 0 ? declared : fallback;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* How a column of each table is keyed in the Drizzle object.
|
|
659
|
+
*
|
|
660
|
+
* The wire name, which is the property key — `authorId`, never `author_id` —
|
|
661
|
+
* because that is what every read and write path in this driver indexes tables
|
|
662
|
+
* and rows by, and what the generated module has always emitted. Getting this
|
|
663
|
+
* wrong does not throw: drizzle leaves an unknown key out of the statement and
|
|
664
|
+
* the write reports success having stored nothing.
|
|
665
|
+
*
|
|
666
|
+
* Two exceptions, both columns no property declares:
|
|
667
|
+
*
|
|
668
|
+
* - the generated search columns keep their raw names, because `search_vector`
|
|
669
|
+
* is how `searchColumnNames` excludes them from responses and how the
|
|
670
|
+
* condition builder finds the one to match against;
|
|
671
|
+
* - a table no collection claims (a junction) is keyed by its **key** columns'
|
|
672
|
+
* names, which is also what the generated file does for them — but a
|
|
673
|
+
* junction that declares `through.properties` keys those payload columns by
|
|
674
|
+
* their property key, for the same reason a collection does. `joinedAt` with
|
|
675
|
+
* `columnName: "joined_at"` has to be one name on both sides or a `_pivot`
|
|
676
|
+
* write is silently dropped by drizzle and the read serves a key the caller
|
|
677
|
+
* never declared.
|
|
678
|
+
*/
|
|
679
|
+
function columnKeysFromCollections(collections) {
|
|
680
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
681
|
+
const rawColumns = /* @__PURE__ */ new Map();
|
|
682
|
+
for (const collection of collections) {
|
|
683
|
+
const table = bareTableName(getTableName$1(collection));
|
|
684
|
+
if (!table) continue;
|
|
685
|
+
byTable.set(table, collection);
|
|
686
|
+
rawColumns.set(table, new Set(searchColumnNames(collection)));
|
|
687
|
+
}
|
|
688
|
+
const junctionByTable = /* @__PURE__ */ new Map();
|
|
689
|
+
for (const spec of resolveJunctionSpecs(collections).values()) {
|
|
690
|
+
if (Object.keys(spec.properties).length === 0) continue;
|
|
691
|
+
junctionByTable.set(bareTableName(spec.table), getJunctionCollectionConfig(spec));
|
|
692
|
+
}
|
|
693
|
+
return (tableName, columnName) => {
|
|
694
|
+
const collection = byTable.get(tableName);
|
|
695
|
+
if (!collection) {
|
|
696
|
+
const junction = junctionByTable.get(tableName);
|
|
697
|
+
return junction ? fieldKeyForColumn(junction, columnName) : columnName;
|
|
698
|
+
}
|
|
699
|
+
if (rawColumns.get(tableName)?.has(columnName)) return columnName;
|
|
700
|
+
return fieldKeyForColumn(collection, columnName);
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Read the tables this data source serves out of `information_schema`.
|
|
705
|
+
*
|
|
706
|
+
* Restricted to the collections handed in, because the driver is given every
|
|
707
|
+
* collection the project declares while each database holds only its own: an
|
|
708
|
+
* unfiltered read had a second source (`database("analytics")`) reporting the
|
|
709
|
+
* default's tables as its own. Junction tables come too — they back no
|
|
710
|
+
* collection and a many-to-many is unreadable without them.
|
|
711
|
+
*
|
|
712
|
+
* Every schema the collections name is read, not just `public`. The users
|
|
713
|
+
* collection lives in `rebase`, and a run restricted to `public` would leave the
|
|
714
|
+
* registry with no table for it at all.
|
|
715
|
+
*/
|
|
716
|
+
async function readCatalogueSchema(options) {
|
|
717
|
+
const { client, collections, defaultSchema } = options;
|
|
718
|
+
/** schema → the tables wanted from it. */
|
|
719
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
720
|
+
const want = (schema, table) => {
|
|
721
|
+
const set = wanted.get(schema) ?? /* @__PURE__ */ new Set();
|
|
722
|
+
wanted.set(schema, set);
|
|
723
|
+
set.add(table);
|
|
724
|
+
};
|
|
725
|
+
for (const collection of collections) {
|
|
726
|
+
const table = bareTableName(getTableName$1(collection));
|
|
727
|
+
if (table) want(schemaOfCollection(collection, defaultSchema), table);
|
|
728
|
+
}
|
|
729
|
+
for (const spec of resolveJunctionSpecs(collections).values()) want(spec.schema, bareTableName(spec.table));
|
|
730
|
+
const columnKey = columnKeysFromCollections(collections);
|
|
731
|
+
const tables = {};
|
|
732
|
+
const meta = /* @__PURE__ */ new Map();
|
|
733
|
+
const missing = [];
|
|
734
|
+
for (const [schema, names] of wanted) {
|
|
735
|
+
const live = await introspectSchema(client, schema);
|
|
736
|
+
const present = /* @__PURE__ */ new Map();
|
|
737
|
+
for (const name of names) {
|
|
738
|
+
const found = live.tablesMap.get(name);
|
|
739
|
+
if (!found) {
|
|
740
|
+
missing.push(schema === defaultSchema ? name : `${schema}.${name}`);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (tables[name]) continue;
|
|
744
|
+
present.set(name, found);
|
|
745
|
+
meta.set(name, found);
|
|
746
|
+
}
|
|
747
|
+
Object.assign(tables, buildDrizzleTablesFromSchema(present, schema, columnKey));
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
tables,
|
|
751
|
+
relations: buildDrizzleRelationsFromCollections(collections, tables),
|
|
752
|
+
meta,
|
|
753
|
+
missing
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The BaaS variant: no collections are declared, so every table in one schema is
|
|
758
|
+
* served and both the collections and the relations are derived from the
|
|
759
|
+
* foreign keys.
|
|
760
|
+
*
|
|
761
|
+
* Kept apart from {@link readCatalogueSchema} because the two answer different
|
|
762
|
+
* questions. Here the database is the entire specification; there it supplies
|
|
763
|
+
* the columns for a specification the project already wrote down.
|
|
764
|
+
*/
|
|
765
|
+
function buildBaasSchema(tablesMap, pgSchemaName, collections) {
|
|
766
|
+
const columnKey = columnKeysFromCollections(collections);
|
|
767
|
+
const tables = buildDrizzleTablesFromSchema(tablesMap, pgSchemaName, columnKey);
|
|
768
|
+
return {
|
|
769
|
+
tables,
|
|
770
|
+
relations: buildDrizzleRelationsFromSchema(tablesMap, tables, columnKey)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/schema/generated-schema-diff.ts
|
|
775
|
+
/**
|
|
776
|
+
* Compare the committed `schema.generated.ts` against the live catalogue, and
|
|
777
|
+
* say so — once per difference, at boot, as a warning and nothing more.
|
|
778
|
+
*
|
|
779
|
+
* The runtime stopped reading that file for its tables (see
|
|
780
|
+
* `catalogue-schema.ts`), which removes an entire class of production failure
|
|
781
|
+
* and creates a smaller one: a stale file is now *invisible*. It is still real —
|
|
782
|
+
* it is what Atlas plans migrations from, what `db push` diffs, what `eject`
|
|
783
|
+
* writes out and what user code imports — so a project whose file no longer
|
|
784
|
+
* describes its database will hit it later, in a tool, with no clue that boot
|
|
785
|
+
* already knew.
|
|
786
|
+
*
|
|
787
|
+
* Hence this: a guard rather than a doc. It never fails boot and never changes
|
|
788
|
+
* what is served — the previous arrangement, where the file could do both,
|
|
789
|
+
* is exactly what was removed.
|
|
790
|
+
*
|
|
791
|
+
* `generated-schema-staleness.ts` answers a narrower question (does the file
|
|
792
|
+
* name every table the collections do) from the file's *source text*. This one
|
|
793
|
+
* compares the loaded module against the database, column by column.
|
|
794
|
+
*/
|
|
795
|
+
/**
|
|
796
|
+
* The families a boot warning is worth raising over.
|
|
797
|
+
*
|
|
798
|
+
* Deliberately coarse. `varchar(120)` and `text` are the same family because the
|
|
799
|
+
* runtime treats them identically and a warning about the difference would fire
|
|
800
|
+
* on every boot of a correct project — the widths still reach Postgres, which
|
|
801
|
+
* enforces them. What this is looking for is the kind of drift that changes what
|
|
802
|
+
* a value *is*: a number that became text, a timestamp that became a date, a
|
|
803
|
+
* column that stopped existing.
|
|
804
|
+
*/
|
|
805
|
+
function typeFamily(sqlType) {
|
|
806
|
+
if (!sqlType) return "unknown";
|
|
807
|
+
let type = sqlType.trim().toLowerCase();
|
|
808
|
+
const isArray = type.endsWith("[]") || type.startsWith("_");
|
|
809
|
+
type = type.replace(/\[\]$/, "").replace(/^_/, "");
|
|
810
|
+
type = type.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
811
|
+
const family = FAMILIES[type] ?? type;
|
|
812
|
+
return isArray ? `${family}[]` : family;
|
|
813
|
+
}
|
|
814
|
+
var FAMILIES = {
|
|
815
|
+
text: "text",
|
|
816
|
+
varchar: "text",
|
|
817
|
+
"character varying": "text",
|
|
818
|
+
bpchar: "text",
|
|
819
|
+
char: "text",
|
|
820
|
+
character: "text",
|
|
821
|
+
citext: "text",
|
|
822
|
+
name: "text",
|
|
823
|
+
int2: "integer",
|
|
824
|
+
smallint: "integer",
|
|
825
|
+
smallserial: "integer",
|
|
826
|
+
int4: "integer",
|
|
827
|
+
int: "integer",
|
|
828
|
+
integer: "integer",
|
|
829
|
+
serial: "integer",
|
|
830
|
+
int8: "bigint",
|
|
831
|
+
bigint: "bigint",
|
|
832
|
+
bigserial: "bigint",
|
|
833
|
+
float4: "float",
|
|
834
|
+
real: "float",
|
|
835
|
+
float8: "float",
|
|
836
|
+
"double precision": "float",
|
|
837
|
+
numeric: "numeric",
|
|
838
|
+
decimal: "numeric",
|
|
839
|
+
money: "numeric",
|
|
840
|
+
bool: "boolean",
|
|
841
|
+
boolean: "boolean",
|
|
842
|
+
uuid: "uuid",
|
|
843
|
+
json: "json",
|
|
844
|
+
jsonb: "jsonb",
|
|
845
|
+
date: "date",
|
|
846
|
+
time: "time",
|
|
847
|
+
"time without time zone": "time",
|
|
848
|
+
timetz: "time",
|
|
849
|
+
"time with time zone": "time",
|
|
850
|
+
timestamp: "timestamp",
|
|
851
|
+
"timestamp without time zone": "timestamp",
|
|
852
|
+
timestamptz: "timestamptz",
|
|
853
|
+
"timestamp with time zone": "timestamptz",
|
|
854
|
+
interval: "interval",
|
|
855
|
+
bytea: "bytea",
|
|
856
|
+
tsvector: "tsvector",
|
|
857
|
+
tsquery: "tsquery",
|
|
858
|
+
vector: "vector"
|
|
859
|
+
};
|
|
860
|
+
function catalogueColumns(meta) {
|
|
861
|
+
const columns = /* @__PURE__ */ new Map();
|
|
862
|
+
for (const col of meta.columns) columns.set(col.column_name, {
|
|
863
|
+
name: col.column_name,
|
|
864
|
+
family: typeFamily(col.udt_name || col.data_type),
|
|
865
|
+
notNull: col.is_nullable === "NO"
|
|
866
|
+
});
|
|
867
|
+
return columns;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Every way the generated module and the database disagree, for the tables both
|
|
871
|
+
* describe.
|
|
872
|
+
*
|
|
873
|
+
* One-directional in one respect only: a table the module has and the database
|
|
874
|
+
* does not is reported (that is the stale-file case), while a table the database
|
|
875
|
+
* has and the module does not is left alone — a database may hold anything, and
|
|
876
|
+
* the collections-vs-file question is `generated-schema-staleness`'s.
|
|
877
|
+
*/
|
|
878
|
+
function diffGeneratedSchemaAgainstCatalogue(options) {
|
|
879
|
+
const { generated, catalogue, collections } = options;
|
|
880
|
+
if (!generated) return [];
|
|
881
|
+
const collectionByTable = /* @__PURE__ */ new Map();
|
|
882
|
+
for (const collection of collections) {
|
|
883
|
+
const table = getTableName$1(collection);
|
|
884
|
+
if (table) collectionByTable.set(bareTableName(table), collection);
|
|
885
|
+
}
|
|
886
|
+
const differences = [];
|
|
887
|
+
for (const value of Object.values(generated)) {
|
|
888
|
+
if (!isTable(value)) continue;
|
|
889
|
+
const table = getTableName(value);
|
|
890
|
+
const collection = collectionByTable.get(table);
|
|
891
|
+
const at = {
|
|
892
|
+
table,
|
|
893
|
+
collection: collection?.slug
|
|
894
|
+
};
|
|
895
|
+
const meta = catalogue.get(table);
|
|
896
|
+
if (!meta) {
|
|
897
|
+
if (collection) differences.push({
|
|
898
|
+
...at,
|
|
899
|
+
kind: "missing-table"
|
|
900
|
+
});
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const live = catalogueColumns(meta);
|
|
904
|
+
const seen = /* @__PURE__ */ new Set();
|
|
905
|
+
const authOwned = isAuthCollection(collection) ? new Set(AUTH_USERS_COLUMNS.map((c) => c.column)) : void 0;
|
|
906
|
+
for (const [key, column] of Object.entries(getTableColumns(value))) {
|
|
907
|
+
const name = column.name ?? key;
|
|
908
|
+
seen.add(name);
|
|
909
|
+
const property = collection ? fieldKeyForColumn(collection, name) : key;
|
|
910
|
+
const generatedFamily = typeFamily(column.getSQLType?.());
|
|
911
|
+
const generatedNotNull = Boolean(column.notNull);
|
|
912
|
+
const found = live.get(name);
|
|
913
|
+
if (!found) {
|
|
914
|
+
differences.push({
|
|
915
|
+
...at,
|
|
916
|
+
kind: "missing-column",
|
|
917
|
+
column: name,
|
|
918
|
+
property,
|
|
919
|
+
generated: generatedFamily
|
|
920
|
+
});
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (found.family !== generatedFamily) differences.push({
|
|
924
|
+
...at,
|
|
925
|
+
kind: "type",
|
|
926
|
+
column: name,
|
|
927
|
+
property,
|
|
928
|
+
generated: generatedFamily,
|
|
929
|
+
live: found.family
|
|
930
|
+
});
|
|
931
|
+
if (found.notNull !== generatedNotNull && !authOwned?.has(name)) differences.push({
|
|
932
|
+
...at,
|
|
933
|
+
kind: "nullability",
|
|
934
|
+
column: name,
|
|
935
|
+
property,
|
|
936
|
+
generated: generatedNotNull ? "NOT NULL" : "nullable",
|
|
937
|
+
live: found.notNull ? "NOT NULL" : "nullable"
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
for (const [name, column] of live) {
|
|
941
|
+
if (seen.has(name)) continue;
|
|
942
|
+
if (authOwned?.has(name)) continue;
|
|
943
|
+
differences.push({
|
|
944
|
+
...at,
|
|
945
|
+
kind: "extra-column",
|
|
946
|
+
column: name,
|
|
947
|
+
property: collection ? fieldKeyForColumn(collection, name) : name,
|
|
948
|
+
live: column.family
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return differences;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Boot's drift question, asked of a database from outside a boot.
|
|
956
|
+
*
|
|
957
|
+
* The same two calls boot makes before it warns, with the arguments it makes
|
|
958
|
+
* them with: the catalogue read the way the registry reads it
|
|
959
|
+
* ({@link readCatalogueSchema}), then {@link diffGeneratedSchemaAgainstCatalogue}
|
|
960
|
+
* over its metadata. `rebase doctor` asks through here, because the boot
|
|
961
|
+
* warning sends the reader to the doctor for "the full report" — and while the
|
|
962
|
+
* doctor compared on its own terms, it answered "In sync" over a NOT NULL and a
|
|
963
|
+
* left-behind column that boot had just warned about.
|
|
964
|
+
*
|
|
965
|
+
* `collections` are the data source's own, as boot filters them.
|
|
966
|
+
*/
|
|
967
|
+
async function readGeneratedSchemaDrift(options) {
|
|
968
|
+
const { client, generated, collections, defaultSchema } = options;
|
|
969
|
+
if (!generated || collections.length === 0) return [];
|
|
970
|
+
return diffGeneratedSchemaAgainstCatalogue({
|
|
971
|
+
generated,
|
|
972
|
+
catalogue: (await readCatalogueSchema({
|
|
973
|
+
client,
|
|
974
|
+
collections,
|
|
975
|
+
defaultSchema
|
|
976
|
+
})).meta,
|
|
977
|
+
collections
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
/** One line, in the terms the reader has: collection, property, column. */
|
|
981
|
+
function describeSchemaDifference(difference) {
|
|
982
|
+
const where = difference.collection ? `${difference.collection} (table "${difference.table}")` : `table "${difference.table}"`;
|
|
983
|
+
const field = difference.column ? `${difference.property && difference.property !== difference.column ? `${difference.property} → "${difference.column}"` : `"${difference.column}"`}` : void 0;
|
|
984
|
+
switch (difference.kind) {
|
|
985
|
+
case "missing-table": return `${where} is in schema.generated.ts but not in the database`;
|
|
986
|
+
case "missing-column": return `${where}: ${field} is in schema.generated.ts but not in the database`;
|
|
987
|
+
case "extra-column": return `${where}: ${field} is in the database but not in schema.generated.ts (${difference.live})`;
|
|
988
|
+
case "type": return `${where}: ${field} is ${difference.generated} in schema.generated.ts and ${difference.live} in the database`;
|
|
989
|
+
case "nullability": return `${where}: ${field} is ${difference.generated} in schema.generated.ts and ${difference.live} in the database`;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Report the differences, one warning each, and never throw.
|
|
994
|
+
*
|
|
995
|
+
* Per difference rather than one summary block on purpose: these are read out of
|
|
996
|
+
* a log aggregator as often as a terminal, and a single multi-line message
|
|
997
|
+
* cannot be filtered down to the one collection somebody is looking at.
|
|
998
|
+
*
|
|
999
|
+
* The fix it names is `rebase db push`. `rebase dev` rewrites
|
|
1000
|
+
* schema.generated.ts from the collections on every change, and boot applies
|
|
1001
|
+
* only additive changes, so what is still different at boot is the database
|
|
1002
|
+
* behind the file: a rename, a dropped column, a narrowed type, a NOT NULL.
|
|
1003
|
+
* `rebase schema generate` rewrites the file and leaves the database as it is,
|
|
1004
|
+
* so it is named only for collections that changed since the file was written.
|
|
1005
|
+
*/
|
|
1006
|
+
function warnOnGeneratedSchemaDrift(differences) {
|
|
1007
|
+
if (differences.length === 0) return;
|
|
1008
|
+
logger.warn(`📄 [schema] schema.generated.ts and the database disagree (${differences.length} difference${differences.length === 1 ? "" : "s"}). Boot applies only additive changes; these are the ones it left for you. Run \`rebase db push\` to apply them (it shows the plan and asks before anything destructive), or \`rebase db generate\` for a migration. If the collections changed since schema.generated.ts was written, run \`rebase schema generate\` first.`);
|
|
1009
|
+
for (const difference of differences) logger.warn(` • ${describeSchemaDifference(difference)}`, {
|
|
1010
|
+
schemaDrift: {
|
|
1011
|
+
kind: difference.kind,
|
|
1012
|
+
table: difference.table,
|
|
1013
|
+
collection: difference.collection,
|
|
1014
|
+
column: difference.column,
|
|
1015
|
+
property: difference.property,
|
|
1016
|
+
generated: difference.generated,
|
|
1017
|
+
live: difference.live
|
|
1018
|
+
},
|
|
1019
|
+
fix: "rebase db push"
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
//#endregion
|
|
1023
|
+
export { buildBaasSchema as a, introspectSchema as c, warnOnGeneratedSchemaDrift as i, readRlsStatus as l, diffGeneratedSchemaAgainstCatalogue as n, readCatalogueSchema as o, readGeneratedSchemaDrift as r, buildCollectionsFromSchema as s, describeSchemaDifference as t };
|
|
1024
|
+
|
|
1025
|
+
//# sourceMappingURL=generated-schema-diff-eZCklRZQ.js.map
|