@rebasepro/server-postgres 0.12.1-canary.g4e7bcbf → 0.12.1-canary.g6861540
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/services.d.ts +6 -1
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/{backup-service-DLb2drIH.js → backup-service-CD8o_1Sl.js} +136 -10
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/{ensure-collection-policies-Dv21KDMJ.js → ensure-collection-policies-BrUVgjz3.js} +2 -2
- package/dist/{ensure-collection-policies-Dv21KDMJ.js.map → ensure-collection-policies-BrUVgjz3.js.map} +1 -1
- package/dist/{ensure-collection-tables-CT6xHB1d.js → ensure-collection-tables-Da2oGkX2.js} +2 -2
- package/dist/{ensure-collection-tables-CT6xHB1d.js.map → ensure-collection-tables-Da2oGkX2.js.map} +1 -1
- package/dist/index.es.js +256 -88
- package/dist/index.es.js.map +1 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +24 -1
- package/dist/{src-BkdpiQdw.js → src-CzbghKwf.js} +43 -5
- package/dist/src-CzbghKwf.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +5 -4
- package/package.json +6 -6
- package/src/auth/services.ts +6 -3
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli.ts +11 -4
- package/src/collections/validate-relations.ts +15 -0
- package/src/data-transformer.ts +9 -3
- package/src/schema/generate-drizzle-schema-logic.ts +7 -1
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +364 -68
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +211 -45
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +40 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/dist/backup-service-DLb2drIH.js.map +0 -1
- package/dist/src-BkdpiQdw.js.map +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PostgreSQL type → Rebase property type mapping.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `introspect-db-logic` so that the structural analysis can use it
|
|
5
|
+
* without importing the generator, which imports the analysis. Re-exported from
|
|
6
|
+
* `introspect-db-logic` so existing callers keep their import path.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Map a PostgreSQL data type to a Rebase property type.
|
|
11
|
+
*/
|
|
12
|
+
export function mapPgType(dataType: string): string {
|
|
13
|
+
const dt = dataType.toLowerCase();
|
|
14
|
+
|
|
15
|
+
// Interval MUST be checked before numeric ("interval" contains "int")
|
|
16
|
+
if (dt === "interval") return "string";
|
|
17
|
+
|
|
18
|
+
// Array types MUST be checked before numeric ("_int4" contains "int")
|
|
19
|
+
if (dt === "array" || dt.startsWith("_")) return "array";
|
|
20
|
+
|
|
21
|
+
// Numeric types
|
|
22
|
+
if (
|
|
23
|
+
dt.includes("int") || // integer, smallint, bigint
|
|
24
|
+
dt.includes("numeric") ||
|
|
25
|
+
dt.includes("decimal") ||
|
|
26
|
+
dt.includes("serial") || // serial, bigserial
|
|
27
|
+
dt === "real" ||
|
|
28
|
+
dt === "float4" ||
|
|
29
|
+
dt === "float8" ||
|
|
30
|
+
dt === "double precision" ||
|
|
31
|
+
dt === "money"
|
|
32
|
+
) {
|
|
33
|
+
return "number";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Boolean
|
|
37
|
+
if (dt.includes("bool")) return "boolean";
|
|
38
|
+
|
|
39
|
+
// Date / Time
|
|
40
|
+
if (dt.includes("time") || dt.includes("date")) return "date";
|
|
41
|
+
|
|
42
|
+
// JSON
|
|
43
|
+
if (dt === "json" || dt === "jsonb") return "map";
|
|
44
|
+
|
|
45
|
+
// Binary
|
|
46
|
+
if (dt === "bytea") return "binary";
|
|
47
|
+
|
|
48
|
+
// Network types
|
|
49
|
+
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
50
|
+
|
|
51
|
+
// UUID
|
|
52
|
+
if (dt === "uuid") return "string";
|
|
53
|
+
|
|
54
|
+
// Text/varchar/char — default to string
|
|
55
|
+
return "string";
|
|
56
|
+
}
|
|
@@ -7,19 +7,16 @@ import * as dotenv from "dotenv";
|
|
|
7
7
|
import readline from "readline";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
TableRow,
|
|
11
|
-
TableColumn,
|
|
12
|
-
EnumValue,
|
|
13
|
-
PrimaryKeyRow,
|
|
14
|
-
ForeignKeyRow,
|
|
15
10
|
buildTablesMap,
|
|
16
11
|
buildEnumMap,
|
|
17
|
-
identifyJoinTables,
|
|
18
12
|
generateCollectionFile,
|
|
19
13
|
generateIndexContent,
|
|
20
14
|
mergeIndexContent,
|
|
21
15
|
safeHostFromUrl
|
|
22
16
|
} from "./introspect-db-logic";
|
|
17
|
+
import { countRowsUpTo, readSchemaMetadata } from "./introspect-db-queries";
|
|
18
|
+
import { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from "./introspect-db-structure";
|
|
19
|
+
import { parseCheckConstraints } from "./introspect-db-constraints";
|
|
23
20
|
import { logger } from "@rebasepro/server";
|
|
24
21
|
|
|
25
22
|
async function main() {
|
|
@@ -89,82 +86,41 @@ async function main() {
|
|
|
89
86
|
logger.info(chalk.gray(`Introspecting schema '${pgSchema}'...`));
|
|
90
87
|
|
|
91
88
|
try {
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
c.column_default,
|
|
111
|
-
(SELECT a.atttypmod FROM pg_attribute a
|
|
112
|
-
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
113
|
-
WHERE pc.relname = c.table_name
|
|
114
|
-
AND a.attname = c.column_name
|
|
115
|
-
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
116
|
-
FROM information_schema.columns c
|
|
117
|
-
WHERE c.table_schema = $1
|
|
118
|
-
`, [pgSchema]);
|
|
119
|
-
|
|
120
|
-
// 2b. Get Enum Types and their values
|
|
121
|
-
const { rows: enumValues } = await client.query<EnumValue>(`
|
|
122
|
-
SELECT t.typname AS enum_name,
|
|
123
|
-
e.enumlabel AS enum_value,
|
|
124
|
-
e.enumsortorder AS sort_order
|
|
125
|
-
FROM pg_type t
|
|
126
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
127
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
128
|
-
WHERE n.nspname = $1
|
|
129
|
-
ORDER BY t.typname, e.enumsortorder
|
|
130
|
-
`, [pgSchema]);
|
|
131
|
-
|
|
132
|
-
// Build a map: enum_name -> ordered list of values
|
|
133
|
-
const enumMap = buildEnumMap(enumValues);
|
|
134
|
-
|
|
135
|
-
// 3. Get Primary Keys
|
|
136
|
-
const { rows: pks } = await client.query<PrimaryKeyRow>(`
|
|
137
|
-
SELECT t.relname as table_name, a.attname as column_name
|
|
138
|
-
FROM pg_index i
|
|
139
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
140
|
-
AND a.attnum = ANY(i.indkey)
|
|
141
|
-
JOIN pg_class t ON t.oid = i.indrelid
|
|
142
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
143
|
-
WHERE i.indisprimary AND n.nspname = $1
|
|
144
|
-
`, [pgSchema]);
|
|
89
|
+
const metadata = await readSchemaMetadata(client, pgSchema);
|
|
90
|
+
const enumMap = buildEnumMap(metadata.enumValues);
|
|
91
|
+
const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);
|
|
92
|
+
const fks = metadata.fks;
|
|
93
|
+
|
|
94
|
+
// Only tables that could structurally be a code list are counted, and
|
|
95
|
+
// each count stops at the threshold — see `countRowsUpTo`. Introspection
|
|
96
|
+
// runs against a database it does not own, so "cheap on a table of any
|
|
97
|
+
// size" is a requirement, not an optimization.
|
|
98
|
+
for (const table of lookupCandidates(metadata, tablesMap)) {
|
|
99
|
+
try {
|
|
100
|
+
metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// A table this run cannot read is simply not classified as a code
|
|
103
|
+
// list; everything else about it still generates.
|
|
104
|
+
logger.info(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
145
107
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
FROM
|
|
154
|
-
information_schema.table_constraints AS tc
|
|
155
|
-
JOIN information_schema.key_column_usage AS kcu
|
|
156
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
157
|
-
AND tc.table_schema = kcu.table_schema
|
|
158
|
-
JOIN information_schema.constraint_column_usage AS ccu
|
|
159
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
160
|
-
AND ccu.table_schema = tc.table_schema
|
|
161
|
-
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1
|
|
162
|
-
`, [pgSchema]);
|
|
108
|
+
const classifications = classifyTables(metadata, tablesMap);
|
|
109
|
+
const checkFacts = parseCheckConstraints(metadata.checks);
|
|
110
|
+
const joinTables = new Set(
|
|
111
|
+
Array.from(classifications.values())
|
|
112
|
+
.filter((c) => c.role === "junction")
|
|
113
|
+
.map((c) => c.table)
|
|
114
|
+
);
|
|
163
115
|
|
|
164
|
-
const
|
|
165
|
-
|
|
116
|
+
const roleCount = (role: string) =>
|
|
117
|
+
Array.from(classifications.values()).filter((c) => c.role === role).length;
|
|
166
118
|
|
|
167
|
-
logger.info(chalk.blue(`Found ${tablesMap.size} tables
|
|
119
|
+
logger.info(chalk.blue(`Found ${tablesMap.size} tables.`));
|
|
120
|
+
logger.info(chalk.gray(
|
|
121
|
+
` ${roleCount("entity")} entities, ${joinTables.size} join tables (folded into relations), ` +
|
|
122
|
+
`${roleCount("lookup")} code lists, ${roleCount("owned-child")} owned by another table (hidden from navigation).`
|
|
123
|
+
));
|
|
168
124
|
|
|
169
125
|
let runDataInference = false;
|
|
170
126
|
if (args["--no-data-inference"]) {
|
|
@@ -224,7 +180,8 @@ async function main() {
|
|
|
224
180
|
joinTables,
|
|
225
181
|
tablesMap,
|
|
226
182
|
enumMap,
|
|
227
|
-
sampleData
|
|
183
|
+
sampleData,
|
|
184
|
+
{ metadata, classifications, checkFacts }
|
|
228
185
|
);
|
|
229
186
|
|
|
230
187
|
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eq, and, sql, SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
// import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
-
import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany } from "@rebasepro/types";
|
|
4
|
+
import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
5
5
|
import { getTableName, resolveCollectionRelations } from "@rebasepro/common";
|
|
6
6
|
import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
7
7
|
import {
|
|
@@ -240,16 +240,30 @@ export class PersistService {
|
|
|
240
240
|
throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to update.`);
|
|
241
241
|
}
|
|
242
242
|
} else {
|
|
243
|
-
// One-to-many create: stamp the parent's
|
|
243
|
+
// One-to-many create: stamp the parent's key onto the child's FK.
|
|
244
244
|
const targetColumnName = this.resolveParentForeignKeyColumn(hop);
|
|
245
245
|
|
|
246
246
|
if (targetColumnName) {
|
|
247
|
-
|
|
247
|
+
// Not necessarily the id in the path: a link with a
|
|
248
|
+
// `sourceKey` is pointed at a column on the parent row, and
|
|
249
|
+
// stamping the id would create a child that belongs to
|
|
250
|
+
// nobody — a row the read path would never return again.
|
|
251
|
+
const parentKeyValue = hasForeignKeyOnTarget(hop.relation)
|
|
252
|
+
? await this.relationService.parentKeyValue(hop.parentCollection, hop.relation, hop.parentId)
|
|
253
|
+
: parentIdForWrite();
|
|
254
|
+
|
|
255
|
+
if (parentKeyValue === undefined) {
|
|
256
|
+
throw ApiError.badRequest(
|
|
257
|
+
`Cannot create under "${collectionPath}": the parent row has no value in ` +
|
|
258
|
+
`\`sourceKey\`, so the new row has nothing to point at.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
|
|
249
|
-
if (existingValue !== undefined && existingValue !== null && existingValue !==
|
|
250
|
-
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent
|
|
263
|
+
if (existingValue !== undefined && existingValue !== null && existingValue !== parentKeyValue) {
|
|
264
|
+
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent key '${parentKeyValue}'.`);
|
|
251
265
|
}
|
|
252
|
-
(effectiveValues as Record<string, unknown>)[targetColumnName] =
|
|
266
|
+
(effectiveValues as Record<string, unknown>)[targetColumnName] = parentKeyValue;
|
|
253
267
|
}
|
|
254
268
|
}
|
|
255
269
|
}
|