@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.
Files changed (47) hide show
  1. package/dist/auth/services.d.ts +6 -1
  2. package/dist/backup/backup-service.d.ts +10 -1
  3. package/dist/backup/pg-tools.d.ts +47 -0
  4. package/dist/{backup-service-DLb2drIH.js → backup-service-CD8o_1Sl.js} +136 -10
  5. package/dist/backup-service-CD8o_1Sl.js.map +1 -0
  6. package/dist/{ensure-collection-policies-Dv21KDMJ.js → ensure-collection-policies-BrUVgjz3.js} +2 -2
  7. package/dist/{ensure-collection-policies-Dv21KDMJ.js.map → ensure-collection-policies-BrUVgjz3.js.map} +1 -1
  8. package/dist/{ensure-collection-tables-CT6xHB1d.js → ensure-collection-tables-Da2oGkX2.js} +2 -2
  9. package/dist/{ensure-collection-tables-CT6xHB1d.js.map → ensure-collection-tables-Da2oGkX2.js.map} +1 -1
  10. package/dist/index.es.js +256 -88
  11. package/dist/index.es.js.map +1 -1
  12. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  13. package/dist/schema/introspect-db-logic.d.ts +94 -5
  14. package/dist/schema/introspect-db-queries.d.ts +119 -0
  15. package/dist/schema/introspect-db-structure.d.ts +263 -0
  16. package/dist/schema/introspect-db-types.d.ts +11 -0
  17. package/dist/services/RelationService.d.ts +24 -1
  18. package/dist/services/channel-bus/index.d.ts +1 -7
  19. package/dist/services/collection-helpers.d.ts +24 -1
  20. package/dist/{src-BkdpiQdw.js → src-CzbghKwf.js} +43 -5
  21. package/dist/src-CzbghKwf.js.map +1 -0
  22. package/dist/utils/connection-string.d.ts +29 -0
  23. package/dist/utils/drizzle-conditions.d.ts +5 -4
  24. package/package.json +6 -6
  25. package/src/auth/services.ts +6 -3
  26. package/src/backup/backup-cli.ts +41 -2
  27. package/src/backup/backup-service.ts +38 -5
  28. package/src/backup/pg-tools.ts +96 -3
  29. package/src/cli.ts +11 -4
  30. package/src/collections/validate-relations.ts +15 -0
  31. package/src/data-transformer.ts +9 -3
  32. package/src/schema/generate-drizzle-schema-logic.ts +7 -1
  33. package/src/schema/introspect-db-constraints.ts +385 -0
  34. package/src/schema/introspect-db-inference.ts +18 -8
  35. package/src/schema/introspect-db-logic.ts +364 -68
  36. package/src/schema/introspect-db-queries.ts +326 -0
  37. package/src/schema/introspect-db-structure.ts +670 -0
  38. package/src/schema/introspect-db-types.ts +56 -0
  39. package/src/schema/introspect-db.ts +37 -80
  40. package/src/services/PersistService.ts +20 -6
  41. package/src/services/RelationService.ts +211 -45
  42. package/src/services/channel-bus/index.ts +0 -9
  43. package/src/services/collection-helpers.ts +40 -1
  44. package/src/utils/connection-string.ts +58 -0
  45. package/src/utils/drizzle-conditions.ts +31 -6
  46. package/dist/backup-service-DLb2drIH.js.map +0 -1
  47. 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
- // 1. Get Tables
93
- const { rows: tables } = await client.query<TableRow>(`
94
- SELECT table_name
95
- FROM information_schema.tables
96
- WHERE table_schema = $1 AND table_type = 'BASE TABLE'
97
- AND table_name NOT LIKE 'drizzle_%'
98
- AND table_name NOT LIKE 'rebase_%'
99
- ORDER BY table_name
100
- `, [pgSchema]);
101
-
102
- // 2. Get Columns
103
- const { rows: columns } = await client.query<TableColumn>(`
104
- SELECT
105
- c.table_name,
106
- c.column_name,
107
- c.data_type,
108
- c.udt_name,
109
- c.is_nullable,
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
- // 4. Get Foreign Keys
147
- const { rows: fks } = await client.query<ForeignKeyRow>(`
148
- SELECT
149
- tc.table_name,
150
- kcu.column_name,
151
- ccu.table_name AS foreign_table_name,
152
- ccu.column_name AS foreign_column_name
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 tablesMap = buildTablesMap(tables, columns, pks, fks);
165
- const joinTables = identifyJoinTables(tablesMap);
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 (including ${joinTables.size} detected join 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 id onto the child's FK.
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
- const parsedParentId = parentIdForWrite();
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 !== parsedParentId) {
250
- logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
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] = parsedParentId;
266
+ (effectiveValues as Record<string, unknown>)[targetColumnName] = parentKeyValue;
253
267
  }
254
268
  }
255
269
  }