@rebasepro/server-postgresql 0.0.1-canary.eae7889 → 0.0.1-canary.f81da60

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 (32) hide show
  1. package/dist/index.es.js +171 -180
  2. package/dist/index.es.js.map +1 -1
  3. package/dist/index.umd.js +171 -180
  4. package/dist/index.umd.js.map +1 -1
  5. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +6 -0
  6. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  7. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  8. package/dist/types/src/controllers/auth.d.ts +2 -2
  9. package/dist/types/src/controllers/collection_registry.d.ts +2 -1
  10. package/dist/types/src/controllers/data_driver.d.ts +36 -1
  11. package/dist/types/src/types/backend_hooks.d.ts +187 -0
  12. package/dist/types/src/types/collections.d.ts +11 -10
  13. package/dist/types/src/types/cron.d.ts +1 -1
  14. package/dist/types/src/types/entity_views.d.ts +4 -6
  15. package/dist/types/src/types/formex.d.ts +40 -0
  16. package/dist/types/src/types/index.d.ts +2 -0
  17. package/dist/types/src/types/plugins.d.ts +6 -3
  18. package/dist/types/src/types/properties.d.ts +61 -89
  19. package/dist/types/src/types/slots.d.ts +20 -10
  20. package/dist/types/src/types/translations.d.ts +4 -0
  21. package/package.json +6 -5
  22. package/src/PostgresBackendDriver.ts +9 -0
  23. package/src/cli.ts +59 -1
  24. package/src/schema/generate-drizzle-schema-logic.ts +7 -25
  25. package/src/schema/introspect-db-logic.ts +592 -0
  26. package/src/schema/introspect-db.ts +211 -0
  27. package/src/services/EntityPersistService.ts +7 -1
  28. package/test/generate-drizzle-schema.test.ts +47 -0
  29. package/test/introspect-db-generation.test.ts +436 -0
  30. package/test/introspect-db-utils.test.ts +392 -0
  31. package/test/relations.test.ts +4 -4
  32. package/test/unmapped-tables-safety.test.ts +345 -0
@@ -0,0 +1,211 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import pg from "pg";
5
+ import arg from "arg";
6
+ import * as dotenv from "dotenv";
7
+
8
+ import {
9
+ TableRow,
10
+ TableColumn,
11
+ EnumValue,
12
+ PrimaryKeyRow,
13
+ ForeignKeyRow,
14
+ buildTablesMap,
15
+ buildEnumMap,
16
+ identifyJoinTables,
17
+ generateCollectionFile,
18
+ generateIndexContent,
19
+ mergeIndexContent,
20
+ safeHostFromUrl,
21
+ } from "./introspect-db-logic";
22
+
23
+ async function main() {
24
+ const args = arg(
25
+ {
26
+ "--output": String,
27
+ "--force": Boolean,
28
+ "--schema": String,
29
+ "-o": "--output",
30
+ "-f": "--force",
31
+ },
32
+ { permissive: true }
33
+ );
34
+
35
+ const outDir = args["--output"] || path.resolve(process.cwd(), "config", "collections");
36
+ const force = args["--force"] || false;
37
+ const pgSchema = args["--schema"] || "public";
38
+
39
+ if (!fs.existsSync(outDir)) {
40
+ fs.mkdirSync(outDir, { recursive: true });
41
+ }
42
+
43
+ // Load env
44
+ const envPaths = [
45
+ process.env.DOTENV_CONFIG_PATH,
46
+ path.resolve(process.cwd(), ".env"),
47
+ path.resolve(process.cwd(), "../.env"),
48
+ path.resolve(process.cwd(), "../../.env")
49
+ ].filter(Boolean) as string[];
50
+
51
+ for (const p of envPaths) {
52
+ if (fs.existsSync(p)) {
53
+ dotenv.config({ path: p });
54
+ break;
55
+ }
56
+ }
57
+
58
+ const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
59
+ if (!databaseUrl) {
60
+ console.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
61
+ process.exit(1);
62
+ }
63
+
64
+ const client = new pg.Client({ connectionString: databaseUrl });
65
+
66
+ try {
67
+ await client.connect();
68
+ } catch (err) {
69
+ console.error(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
70
+ console.error(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
71
+ process.exit(1);
72
+ }
73
+
74
+ // Log the host portion safely — handle URLs without "@"
75
+ const hostPart = safeHostFromUrl(databaseUrl);
76
+ console.log(chalk.gray(`Connected to database: ${hostPart}`));
77
+ console.log(chalk.gray(`Introspecting schema '${pgSchema}'...`));
78
+
79
+ try {
80
+ // 1. Get Tables
81
+ const { rows: tables } = await client.query<TableRow>(`
82
+ SELECT table_name
83
+ FROM information_schema.tables
84
+ WHERE table_schema = $1 AND table_type = 'BASE TABLE'
85
+ AND table_name NOT LIKE 'drizzle_%'
86
+ AND table_name NOT LIKE 'rebase_%'
87
+ ORDER BY table_name
88
+ `, [pgSchema]);
89
+
90
+ // 2. Get Columns
91
+ const { rows: columns } = await client.query<TableColumn>(`
92
+ SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default
93
+ FROM information_schema.columns
94
+ WHERE table_schema = $1
95
+ `, [pgSchema]);
96
+
97
+ // 2b. Get Enum Types and their values
98
+ const { rows: enumValues } = await client.query<EnumValue>(`
99
+ SELECT t.typname AS enum_name,
100
+ e.enumlabel AS enum_value,
101
+ e.enumsortorder AS sort_order
102
+ FROM pg_type t
103
+ JOIN pg_enum e ON t.oid = e.enumtypid
104
+ JOIN pg_namespace n ON t.typnamespace = n.oid
105
+ WHERE n.nspname = $1
106
+ ORDER BY t.typname, e.enumsortorder
107
+ `, [pgSchema]);
108
+
109
+ // Build a map: enum_name -> ordered list of values
110
+ const enumMap = buildEnumMap(enumValues);
111
+
112
+ // 3. Get Primary Keys
113
+ const { rows: pks } = await client.query<PrimaryKeyRow>(`
114
+ SELECT t.relname as table_name, a.attname as column_name
115
+ FROM pg_index i
116
+ JOIN pg_attribute a ON a.attrelid = i.indrelid
117
+ AND a.attnum = ANY(i.indkey)
118
+ JOIN pg_class t ON t.oid = i.indrelid
119
+ JOIN pg_namespace n ON n.oid = t.relnamespace
120
+ WHERE i.indisprimary AND n.nspname = $1
121
+ `, [pgSchema]);
122
+
123
+ // 4. Get Foreign Keys
124
+ const { rows: fks } = await client.query<ForeignKeyRow>(`
125
+ SELECT
126
+ tc.table_name,
127
+ kcu.column_name,
128
+ ccu.table_name AS foreign_table_name,
129
+ ccu.column_name AS foreign_column_name
130
+ FROM
131
+ information_schema.table_constraints AS tc
132
+ JOIN information_schema.key_column_usage AS kcu
133
+ ON tc.constraint_name = kcu.constraint_name
134
+ AND tc.table_schema = kcu.table_schema
135
+ JOIN information_schema.constraint_column_usage AS ccu
136
+ ON ccu.constraint_name = tc.constraint_name
137
+ AND ccu.table_schema = tc.table_schema
138
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1
139
+ `, [pgSchema]);
140
+
141
+ const tablesMap = buildTablesMap(tables, columns, pks, fks);
142
+ const joinTables = identifyJoinTables(tablesMap);
143
+
144
+ console.log(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
145
+
146
+ // Generate Collections
147
+ const generatedFiles: string[] = [];
148
+ const skippedFiles: string[] = [];
149
+
150
+ for (const [tableName, meta] of tablesMap.entries()) {
151
+ if (joinTables.has(tableName)) continue; // We don't generate base collections for pure join tables
152
+
153
+ // ── File overwrite protection ──────────────────────────────
154
+ const filePath = path.join(outDir, `${tableName}.ts`);
155
+ if (fs.existsSync(filePath) && !force) {
156
+ skippedFiles.push(tableName);
157
+ continue;
158
+ }
159
+
160
+ const fileContent = generateCollectionFile(
161
+ tableName,
162
+ meta,
163
+ fks,
164
+ joinTables,
165
+ tablesMap,
166
+ enumMap,
167
+ );
168
+
169
+ fs.writeFileSync(filePath, fileContent, "utf-8");
170
+ generatedFiles.push(tableName);
171
+ console.log(chalk.green(` ✓ ${filePath}`));
172
+ }
173
+
174
+ // Generate index.ts (sorted alphabetically for deterministic output)
175
+ if (generatedFiles.length > 0) {
176
+ const indexPath = path.join(outDir, "index.ts");
177
+
178
+ if (fs.existsSync(indexPath) && !force) {
179
+ // Merge: read existing index, add new exports that don't already exist
180
+ const existing = fs.readFileSync(indexPath, "utf-8");
181
+ const merged = mergeIndexContent(existing, generatedFiles);
182
+ fs.writeFileSync(indexPath, merged, "utf-8");
183
+ } else {
184
+ const indexContent = generateIndexContent(generatedFiles);
185
+ fs.writeFileSync(indexPath, indexContent, "utf-8");
186
+ }
187
+ console.log(chalk.green(` ✓ ${indexPath}`));
188
+ }
189
+
190
+ console.log("");
191
+ if (skippedFiles.length > 0) {
192
+ console.log(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
193
+ console.log(chalk.gray(` Use --force to overwrite existing files.`));
194
+ console.log("");
195
+ }
196
+ console.log(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
197
+ console.log(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
198
+ console.log("");
199
+
200
+ } catch (e) {
201
+ console.error(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
202
+ process.exit(1);
203
+ } finally {
204
+ await client.end();
205
+ }
206
+ }
207
+
208
+ main().catch((err) => {
209
+ console.error(err);
210
+ process.exit(1);
211
+ });
@@ -111,7 +111,7 @@ export class EntityPersistService {
111
111
  targetColumnName = relation.localKey;
112
112
  } else if (relation.foreignKeyOnTarget) {
113
113
  targetColumnName = relation.foreignKeyOnTarget;
114
- } else if (relation.joinPath && relation.joinPath.length > 0) {
114
+ } else if (relation.joinPath && relation.joinPath.length === 1) {
115
115
  const targetTableName = getTableName(targetCollection);
116
116
  const relevantJoinStep = relation.joinPath.find(joinStep => joinStep.table === targetTableName);
117
117
 
@@ -123,6 +123,12 @@ export class EntityPersistService {
123
123
  const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to);
124
124
  targetColumnName = targetColumnNames[0];
125
125
  }
126
+ } else if (relation.joinPath && relation.joinPath.length > 1) {
127
+ // For multi-hop relations (like many-to-many through a junction table),
128
+ // there is no direct foreign key on the target table pointing to the parent.
129
+ // The relationship is managed via the junction table.
130
+ // We shouldn't inject the parent ID directly into the target entity payload.
131
+ break;
126
132
  } else {
127
133
  throw new Error(`Relation '${relationKey}' lacks configuration for path-based saving.`);
128
134
  }
@@ -666,6 +666,53 @@ using: "{is_locked} = false" }
666
666
  expect(result).toContain('as: "restrictive"');
667
667
  });
668
668
 
669
+ it("should enable RLS on every table even without any security rules", async () => {
670
+ const collections: EntityCollection[] = [{
671
+ slug: "public_data",
672
+ table: "public_data",
673
+ name: "Public Data",
674
+ properties: { title: { type: "string" } }
675
+ // No securityRules defined — table should still have .enableRLS()
676
+ }];
677
+
678
+ const result = await generateSchema(collections);
679
+ expect(result).toContain(".enableRLS()");
680
+ // No policies should be generated
681
+ expect(result).not.toContain("pgPolicy(");
682
+ });
683
+
684
+ it("should enable RLS on tables that do have security rules", async () => {
685
+ const collections: EntityCollection[] = [{
686
+ slug: "secure_data",
687
+ table: "secure_data",
688
+ name: "Secure Data",
689
+ properties: { title: { type: "string" } },
690
+ securityRules: [
691
+ { operation: "select", access: "public" }
692
+ ]
693
+ }];
694
+
695
+ const result = await generateSchema(collections);
696
+ expect(result).toContain(".enableRLS()");
697
+ expect(result).toContain("pgPolicy(");
698
+ });
699
+
700
+ it("should fall back to deny-all (sql`false`) when no USING clause can be generated", async () => {
701
+ const collections: EntityCollection[] = [{
702
+ slug: "deny_test",
703
+ table: "deny_test",
704
+ name: "Deny Test",
705
+ properties: { title: { type: "string" } },
706
+ securityRules: [
707
+ { operation: "select" }
708
+ // No access, ownerField, or using — should produce sql`false`
709
+ ]
710
+ }];
711
+
712
+ const result = await generateSchema(collections);
713
+ expect(result).toContain("sql`false`");
714
+ });
715
+
669
716
  });
670
717
  });
671
718
  // V2 improvements tests