@rebasepro/server-postgresql 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBackendDriver.d.ts +8 -0
- package/dist/auth/services.d.ts +21 -1
- package/dist/cli-errors.d.ts +29 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +2 -2
- package/dist/index.es.js +2987 -230
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-default-policies.d.ts +12 -0
- package/dist/schema/auth-schema.d.ts +227 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/services/entityService.d.ts +1 -1
- package/dist/utils/pg-error-utils.d.ts +10 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/package.json +15 -9
- package/src/PostgresBackendDriver.ts +200 -68
- package/src/PostgresBootstrapper.ts +34 -2
- package/src/auth/ensure-tables.ts +56 -1
- package/src/auth/services.ts +94 -1
- package/src/cli-errors.ts +162 -0
- package/src/cli-helpers.ts +183 -0
- package/src/cli.ts +264 -245
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-default-policies.ts +97 -0
- package/src/schema/auth-schema.ts +25 -2
- package/src/schema/doctor.ts +2 -4
- package/src/schema/generate-drizzle-schema-logic.ts +20 -82
- package/src/schema/generate-drizzle-schema.ts +3 -5
- package/src/schema/generate-postgres-ddl-logic.ts +487 -0
- package/src/schema/generate-postgres-ddl.ts +116 -0
- package/src/services/EntityPersistService.ts +10 -8
- package/src/services/entityService.ts +28 -3
- package/src/services/realtimeService.ts +26 -2
- package/src/utils/pg-error-utils.ts +16 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +9 -0
- package/test/auth-default-policies.test.ts +89 -0
- package/test/cli-helpers-extended.test.ts +324 -0
- package/test/cli-helpers.test.ts +59 -0
- package/test/connection.test.ts +292 -0
- package/test/databasePoolManager.test.ts +289 -0
- package/test/doctor-extended.test.ts +443 -0
- package/test/e2e/db-e2e.test.ts +293 -0
- package/test/e2e/pg-setup.ts +79 -0
- package/test/entity-callbacks-redaction.test.ts +86 -0
- package/test/entity-persist-composite-keys.test.ts +451 -0
- package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
- package/test/generate-postgres-ddl.test.ts +300 -0
- package/test/mfa-service.test.ts +544 -0
- package/test/pg-error-utils.test.ts +50 -1
- package/test/postgresDataDriver.test.ts +2 -1
- package/test/realtimeService-channels.test.ts +696 -0
- package/test/unmapped-tables-safety.test.ts +55 -342
- package/vitest.e2e.config.ts +10 -0
package/src/cli.ts
CHANGED
|
@@ -3,34 +3,51 @@ import chalk from "chalk";
|
|
|
3
3
|
import { execa } from "execa";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import fs from "fs";
|
|
6
|
-
import { execSync } from "child_process";
|
|
7
6
|
import { fileURLToPath } from "url";
|
|
8
7
|
import { logger } from "@rebasepro/server-core";
|
|
8
|
+
import {
|
|
9
|
+
resolveLocalBin,
|
|
10
|
+
getTableIncludes,
|
|
11
|
+
getDevDatabaseUrl,
|
|
12
|
+
ensureDevDatabaseExists,
|
|
13
|
+
getTableExcludes
|
|
14
|
+
} from "./cli-helpers";
|
|
15
|
+
import { checkDatabaseConnectivity, diagnoseDbError } from "./cli-errors";
|
|
9
16
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
while (true) {
|
|
16
|
-
const candidate = path.join(cwd, "node_modules", ".bin", binName);
|
|
17
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
18
|
-
const parent = path.dirname(cwd);
|
|
19
|
-
if (parent === cwd) break;
|
|
20
|
-
cwd = parent;
|
|
21
|
-
}
|
|
22
|
-
// Fall back to globally installed binary via which/where
|
|
17
|
+
const __cliDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async function loadEnv(): Promise<void> {
|
|
23
22
|
try {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
23
|
+
const dotenv = await import("dotenv");
|
|
24
|
+
const envPaths = [
|
|
25
|
+
process.env.DOTENV_CONFIG_PATH,
|
|
26
|
+
path.resolve(process.cwd(), ".env"),
|
|
27
|
+
path.resolve(process.cwd(), "../.env"),
|
|
28
|
+
path.resolve(process.cwd(), "../../.env")
|
|
29
|
+
].filter(Boolean) as string[];
|
|
30
|
+
|
|
31
|
+
for (const p of envPaths) {
|
|
32
|
+
if (fs.existsSync(p)) {
|
|
33
|
+
const parsed = dotenv.config({ path: p });
|
|
34
|
+
if (parsed.parsed) {
|
|
35
|
+
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
36
|
+
if (process.env[key] === undefined) {
|
|
37
|
+
process.env[key] = val;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
27
44
|
} catch {
|
|
28
|
-
//
|
|
45
|
+
// ignore
|
|
29
46
|
}
|
|
30
|
-
return null;
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
export async function runPluginCommand(args: string[]) {
|
|
50
|
+
await loadEnv();
|
|
34
51
|
const domain = args[0]; // "db" or "schema"
|
|
35
52
|
const subcommand = args[1];
|
|
36
53
|
|
|
@@ -47,7 +64,7 @@ export async function runPluginCommand(args: string[]) {
|
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
50
|
-
const VALID_ACTIONS = ["push", "generate", "migrate", "
|
|
67
|
+
const VALID_ACTIONS = ["push", "generate", "migrate", "branch"];
|
|
51
68
|
if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
|
|
52
69
|
logger.error(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
|
|
53
70
|
process.exit(1);
|
|
@@ -58,50 +75,105 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
58
75
|
return;
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
const argsList = arg(
|
|
79
|
+
{
|
|
80
|
+
"--collections": String,
|
|
81
|
+
"-c": "--collections"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
argv: rawArgs.slice(2),
|
|
85
|
+
permissive: true
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
89
|
+
|
|
61
90
|
if (subcommand === "generate") {
|
|
62
91
|
logger.info("");
|
|
63
92
|
logger.info(chalk.bold(" 📦 Rebase DB Generate"));
|
|
64
|
-
logger.info(chalk.gray(" Step 1/2: Generating Drizzle schema from collections..."));
|
|
93
|
+
logger.info(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
65
94
|
logger.info("");
|
|
66
95
|
await schemaCommand("generate", rawArgs);
|
|
96
|
+
await generatePostgresDdlCommand(rawArgs);
|
|
67
97
|
logger.info("");
|
|
68
|
-
logger.info(chalk.gray(" Step 2/2: Generating SQL migration files..."));
|
|
98
|
+
logger.info(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
|
|
69
99
|
logger.info("");
|
|
70
|
-
|
|
71
|
-
await
|
|
100
|
+
const migrationName = argsList._[0] || "migration";
|
|
101
|
+
await runAtlas("migrate", ["diff", migrationName, "--dir", "file://drizzle/migrations", "--to", "file://drizzle/schema.sql"], collectionsPath);
|
|
102
|
+
|
|
103
|
+
// Post-process the newest migration file
|
|
104
|
+
try {
|
|
105
|
+
const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
|
|
106
|
+
if (fs.existsSync(migrationsDir)) {
|
|
107
|
+
const files = fs.readdirSync(migrationsDir);
|
|
108
|
+
const sqlFiles = files
|
|
109
|
+
.filter(f => f.endsWith(".sql"))
|
|
110
|
+
.sort();
|
|
111
|
+
if (sqlFiles.length > 0) {
|
|
112
|
+
const newestMigrationFile = path.join(migrationsDir, sqlFiles[sqlFiles.length - 1]);
|
|
113
|
+
|
|
114
|
+
// Make CREATE SCHEMA idempotent so it doesn't conflict with
|
|
115
|
+
// --revisions-schema (Atlas pre-creates the rebase schema
|
|
116
|
+
// for its revision table before running migrations).
|
|
117
|
+
let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
|
|
118
|
+
migrationContent = migrationContent.replace(
|
|
119
|
+
/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g,
|
|
120
|
+
"CREATE SCHEMA IF NOT EXISTS $1;"
|
|
121
|
+
);
|
|
122
|
+
fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
|
|
123
|
+
|
|
124
|
+
// Append RLS policies
|
|
125
|
+
const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
126
|
+
if (fs.existsSync(policiesFile)) {
|
|
127
|
+
const policiesContent = fs.readFileSync(policiesFile, "utf-8");
|
|
128
|
+
fs.appendFileSync(newestMigrationFile, "\n\n" + policiesContent);
|
|
129
|
+
logger.info(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
|
|
130
|
+
|
|
131
|
+
// Re-hash the migration directory
|
|
132
|
+
logger.info(chalk.gray(" Re-hashing migration files..."));
|
|
133
|
+
await runAtlas("migrate", ["hash", "--dir", "file://drizzle/migrations"], collectionsPath);
|
|
134
|
+
logger.info(chalk.gray(" ✓ Migration directory checksum updated successfully."));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.warn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
|
|
140
|
+
}
|
|
141
|
+
|
|
72
142
|
logger.info("");
|
|
73
143
|
logger.info(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
|
|
74
144
|
logger.info("");
|
|
75
|
-
} else if (subcommand === "pull") {
|
|
76
|
-
logger.info("");
|
|
77
|
-
logger.info(chalk.yellow(" ⚠ \"rebase db pull\" has been removed."));
|
|
78
|
-
logger.info(chalk.gray(" Use \"rebase schema introspect\" instead."));
|
|
79
|
-
logger.info("");
|
|
80
|
-
process.exit(1);
|
|
81
145
|
} else {
|
|
82
146
|
logger.info("");
|
|
83
147
|
logger.info(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
|
|
84
148
|
logger.info("");
|
|
85
149
|
|
|
86
150
|
if (subcommand === "push") {
|
|
87
|
-
logger.info(chalk.gray(" Step 1/
|
|
151
|
+
logger.info(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
88
152
|
logger.info("");
|
|
89
153
|
await schemaCommand("generate", rawArgs);
|
|
154
|
+
await generatePostgresDdlCommand(rawArgs);
|
|
155
|
+
logger.info("");
|
|
156
|
+
logger.info(chalk.gray(" Step 2/3: Pushing schema to database with Atlas..."));
|
|
90
157
|
logger.info("");
|
|
91
|
-
|
|
158
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
159
|
+
if (databaseUrl) {
|
|
160
|
+
await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
161
|
+
}
|
|
162
|
+
await runAtlas("schema", ["apply", "--to", "file://drizzle/schema.sql", "--auto-approve"], collectionsPath);
|
|
92
163
|
logger.info("");
|
|
93
|
-
|
|
164
|
+
|
|
165
|
+
if (databaseUrl) {
|
|
166
|
+
await applyPolicies(databaseUrl);
|
|
167
|
+
} else {
|
|
168
|
+
logger.warn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
|
|
169
|
+
}
|
|
94
170
|
} else if (subcommand === "migrate") {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!fs.existsSync(schemaPath)) {
|
|
99
|
-
logger.info(chalk.yellow(" ⚠ schema.generated.ts not found. Generating schema first..."));
|
|
100
|
-
await schemaCommand("generate", rawArgs);
|
|
171
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
172
|
+
if (databaseUrl) {
|
|
173
|
+
await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
101
174
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await runDrizzleKit(subcommand, rawArgs);
|
|
175
|
+
const extraArgs = argsList._.filter(arg => arg !== "migrate");
|
|
176
|
+
await runAtlas("migrate", ["apply", "--dir", "file://drizzle/migrations", ...extraArgs], collectionsPath);
|
|
105
177
|
}
|
|
106
178
|
|
|
107
179
|
logger.info("");
|
|
@@ -110,6 +182,68 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
110
182
|
}
|
|
111
183
|
}
|
|
112
184
|
|
|
185
|
+
async function ensureAuthSchemaAndFunctions(databaseUrl: string): Promise<void> {
|
|
186
|
+
try {
|
|
187
|
+
const { Client } = await import("pg");
|
|
188
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
189
|
+
await client.connect();
|
|
190
|
+
try {
|
|
191
|
+
await client.query(`
|
|
192
|
+
CREATE SCHEMA IF NOT EXISTS auth;
|
|
193
|
+
CREATE SCHEMA IF NOT EXISTS rebase;
|
|
194
|
+
|
|
195
|
+
CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
|
|
196
|
+
SELECT NULLIF(current_setting('app.user_id', true), '');
|
|
197
|
+
$$ LANGUAGE sql STABLE;
|
|
198
|
+
|
|
199
|
+
CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb AS $$
|
|
200
|
+
SELECT COALESCE(
|
|
201
|
+
NULLIF(current_setting('app.jwt', true), ''),
|
|
202
|
+
'{}'
|
|
203
|
+
)::jsonb;
|
|
204
|
+
$$ LANGUAGE sql STABLE;
|
|
205
|
+
|
|
206
|
+
CREATE OR REPLACE FUNCTION auth.roles() RETURNS text AS $$
|
|
207
|
+
SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
|
|
208
|
+
$$ LANGUAGE sql STABLE;
|
|
209
|
+
`);
|
|
210
|
+
} finally {
|
|
211
|
+
await client.end();
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
logger.warn(chalk.yellow(` ⚠️ Failed to bootstrap auth schema and helper functions: ${err instanceof Error ? err.message : String(err)}`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function applyPolicies(databaseUrl: string): Promise<void> {
|
|
219
|
+
try {
|
|
220
|
+
const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
221
|
+
if (!fs.existsSync(policiesPath)) return;
|
|
222
|
+
|
|
223
|
+
logger.info(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
|
|
224
|
+
logger.info("");
|
|
225
|
+
|
|
226
|
+
const policiesContent = fs.readFileSync(policiesPath, "utf-8");
|
|
227
|
+
const { Client } = await import("pg");
|
|
228
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
229
|
+
await client.connect();
|
|
230
|
+
try {
|
|
231
|
+
await client.query(policiesContent);
|
|
232
|
+
logger.info(chalk.green(" ✓ RLS policies applied successfully."));
|
|
233
|
+
} finally {
|
|
234
|
+
await client.end();
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const hint = diagnoseDbError(err, databaseUrl);
|
|
238
|
+
if (hint) {
|
|
239
|
+
logger.error(hint);
|
|
240
|
+
} else {
|
|
241
|
+
logger.error(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
242
|
+
}
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
113
247
|
async function branchCommand(rawArgs: string[]): Promise<void> {
|
|
114
248
|
const branchAction = rawArgs[2]; // create, list, delete, info
|
|
115
249
|
|
|
@@ -296,118 +430,13 @@ function timeAgo(date: Date): string {
|
|
|
296
430
|
return `${days}d ago`;
|
|
297
431
|
}
|
|
298
432
|
|
|
299
|
-
/**
|
|
300
|
-
* Post-process generated migration files to fix statement ordering issues.
|
|
301
|
-
*
|
|
302
|
-
* Drizzle-kit can emit DROP POLICY statements *after* ALTER TABLE ... ALTER COLUMN
|
|
303
|
-
* for the same table. Postgres rejects this with:
|
|
304
|
-
* "cannot alter type of a column used in a policy definition"
|
|
305
|
-
*
|
|
306
|
-
* This scans the drizzle output directory for the most recently modified .sql file
|
|
307
|
-
* and reorders statements so that DROP POLICY on a table always precedes any
|
|
308
|
-
* ALTER TABLE on that same table.
|
|
309
|
-
*/
|
|
310
|
-
async function fixMigrationStatementOrder(): Promise<void> {
|
|
311
|
-
const drizzleDir = path.join(process.cwd(), "drizzle");
|
|
312
|
-
if (!fs.existsSync(drizzleDir)) return;
|
|
313
|
-
|
|
314
|
-
// Find the most recently modified .sql file
|
|
315
|
-
const sqlFiles = fs.readdirSync(drizzleDir)
|
|
316
|
-
.filter(f => f.endsWith(".sql"))
|
|
317
|
-
.map(f => ({
|
|
318
|
-
name: f,
|
|
319
|
-
mtime: fs.statSync(path.join(drizzleDir, f)).mtimeMs
|
|
320
|
-
}))
|
|
321
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
322
|
-
|
|
323
|
-
if (sqlFiles.length === 0) return;
|
|
324
|
-
|
|
325
|
-
const latestFile = path.join(drizzleDir, sqlFiles[0].name);
|
|
326
|
-
let content = fs.readFileSync(latestFile, "utf-8");
|
|
327
|
-
const originalContent = content;
|
|
328
|
-
|
|
329
|
-
// Replace CREATE SCHEMA with CREATE SCHEMA IF NOT EXISTS to prevent failures
|
|
330
|
-
content = content.replace(/CREATE SCHEMA "([^"]+)";/g, 'CREATE SCHEMA IF NOT EXISTS "$1";');
|
|
331
|
-
|
|
332
|
-
const DELIMITER = "--> statement-breakpoint";
|
|
333
|
-
const parts = content.split(DELIMITER);
|
|
334
|
-
|
|
335
|
-
// Parse each statement to detect DROP POLICY and ALTER TABLE targets
|
|
336
|
-
const dropPolicyRe = /DROP\s+POLICY\s+.+?\s+ON\s+"([^"]+)"/i;
|
|
337
|
-
const alterTableRe = /ALTER\s+TABLE\s+"([^"]+)"\s+ALTER\s+COLUMN/i;
|
|
338
|
-
|
|
339
|
-
// Collect indices of DROP POLICY statements and what tables they target
|
|
340
|
-
const dropPolicyIndices = new Map<string, number[]>(); // table -> indices
|
|
341
|
-
const alterColumnIndices = new Map<string, number>(); // table -> first ALTER index
|
|
342
|
-
|
|
343
|
-
for (let i = 0; i < parts.length; i++) {
|
|
344
|
-
const stmt = parts[i].trim();
|
|
345
|
-
const dropMatch = stmt.match(dropPolicyRe);
|
|
346
|
-
if (dropMatch) {
|
|
347
|
-
const table = dropMatch[1];
|
|
348
|
-
if (!dropPolicyIndices.has(table)) dropPolicyIndices.set(table, []);
|
|
349
|
-
dropPolicyIndices.get(table)!.push(i);
|
|
350
|
-
}
|
|
351
|
-
const alterMatch = stmt.match(alterTableRe);
|
|
352
|
-
if (alterMatch) {
|
|
353
|
-
const table = alterMatch[1];
|
|
354
|
-
if (!alterColumnIndices.has(table)) alterColumnIndices.set(table, i);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
433
|
|
|
358
|
-
// Check if any DROP POLICY comes after an ALTER COLUMN on the same table
|
|
359
|
-
let needsReorder = false;
|
|
360
|
-
for (const [table, dropIndices] of dropPolicyIndices) {
|
|
361
|
-
const firstAlter = alterColumnIndices.get(table);
|
|
362
|
-
if (firstAlter !== undefined) {
|
|
363
|
-
for (const dropIdx of dropIndices) {
|
|
364
|
-
if (dropIdx > firstAlter) {
|
|
365
|
-
needsReorder = true;
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (needsReorder) break;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (!needsReorder) {
|
|
374
|
-
if (content !== originalContent) {
|
|
375
|
-
fs.writeFileSync(latestFile, content, "utf-8");
|
|
376
|
-
}
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
434
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
stmtEntries.sort((a, b) => {
|
|
386
|
-
const aDropMatch = a.stmt.trim().match(dropPolicyRe);
|
|
387
|
-
const bAlterMatch = b.stmt.trim().match(alterTableRe);
|
|
388
|
-
const bDropMatch = b.stmt.trim().match(dropPolicyRe);
|
|
389
|
-
const aAlterMatch = a.stmt.trim().match(alterTableRe);
|
|
390
|
-
|
|
391
|
-
// If a is DROP POLICY on table X and b is ALTER on table X, a goes first
|
|
392
|
-
if (aDropMatch && bAlterMatch && aDropMatch[1] === bAlterMatch[1]) return -1;
|
|
393
|
-
// If b is DROP POLICY on table X and a is ALTER on table X, b goes first
|
|
394
|
-
if (bDropMatch && aAlterMatch && bDropMatch[1] === aAlterMatch[1]) return 1;
|
|
395
|
-
// Otherwise preserve original order
|
|
396
|
-
return a.idx - b.idx;
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
const reordered = stmtEntries.map(e => e.stmt).join(DELIMITER);
|
|
400
|
-
fs.writeFileSync(latestFile, reordered, "utf-8");
|
|
401
|
-
|
|
402
|
-
logger.info(chalk.yellow(` \u26A0 Reordered migration statements in ${sqlFiles[0].name} (DROP POLICY before ALTER COLUMN)`));
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void> {
|
|
406
|
-
const drizzleKitBin = resolveLocalBin("drizzle-kit");
|
|
407
|
-
if (!drizzleKitBin) {
|
|
408
|
-
logger.error(chalk.red("✗ Could not find drizzle-kit binary."));
|
|
409
|
-
const isNpm = (process.env.npm_config_user_agent ?? "").startsWith("npm/") || fs.existsSync(path.join(process.cwd(), "package-lock.json"));
|
|
410
|
-
const installCmd = isNpm ? "npm install -D drizzle-kit" : "pnpm add -D drizzle-kit";
|
|
435
|
+
async function runAtlas(domain: "schema" | "migrate", args: string[], collectionsPath?: string): Promise<void> {
|
|
436
|
+
const atlasBin = resolveLocalBin("atlas");
|
|
437
|
+
if (!atlasBin) {
|
|
438
|
+
logger.error(chalk.red("✗ Could not find atlas binary."));
|
|
439
|
+
const installCmd = "pnpm add -D @ariga/atlas";
|
|
411
440
|
logger.error(chalk.gray(` Install it with: ${installCmd}`));
|
|
412
441
|
process.exit(1);
|
|
413
442
|
}
|
|
@@ -436,109 +465,100 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
436
465
|
}
|
|
437
466
|
}
|
|
438
467
|
} catch {
|
|
439
|
-
//
|
|
468
|
+
// ignore
|
|
440
469
|
}
|
|
441
470
|
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const drizzleKitArgs = [action];
|
|
447
|
-
if (action === "push") {
|
|
448
|
-
drizzleKitArgs.push("--strict", "--verbose");
|
|
471
|
+
const databaseUrl = env.DATABASE_URL;
|
|
472
|
+
if (!databaseUrl) {
|
|
473
|
+
logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
474
|
+
process.exit(1);
|
|
449
475
|
}
|
|
450
476
|
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
477
|
+
// Pre-flight: verify the database is reachable before running Atlas.
|
|
478
|
+
// This catches ECONNREFUSED / auth failures with a friendly banner
|
|
479
|
+
// instead of letting Atlas surface a raw error.
|
|
480
|
+
await checkDatabaseConnectivity(databaseUrl);
|
|
481
|
+
|
|
482
|
+
const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
|
|
483
|
+
await ensureDevDatabaseExists(databaseUrl, devDatabaseUrl);
|
|
484
|
+
|
|
485
|
+
const atlasArgs = [domain, ...args];
|
|
486
|
+
|
|
487
|
+
if (domain === "schema") {
|
|
488
|
+
if (args.includes("apply")) {
|
|
489
|
+
atlasArgs.push("--url", databaseUrl, "--dev-url", devDatabaseUrl);
|
|
490
|
+
} else if (args.includes("clean") || args.includes("inspect")) {
|
|
491
|
+
atlasArgs.push("--url", databaseUrl);
|
|
492
|
+
}
|
|
493
|
+
} else if (domain === "migrate") {
|
|
494
|
+
if (args.includes("diff")) {
|
|
495
|
+
atlasArgs.push("--dev-url", devDatabaseUrl);
|
|
496
|
+
} else if (args.includes("apply") || args.includes("status")) {
|
|
497
|
+
atlasArgs.push("--url", databaseUrl, "--revisions-schema", "rebase");
|
|
498
|
+
if (args.includes("apply")) {
|
|
499
|
+
atlasArgs.push("--allow-dirty");
|
|
459
500
|
}
|
|
460
|
-
continue;
|
|
461
501
|
}
|
|
462
|
-
|
|
463
|
-
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (domain === "schema" && args.includes("apply") && collectionsPath) {
|
|
505
|
+
const excludes = await getTableExcludes(databaseUrl, collectionsPath);
|
|
506
|
+
for (const exc of excludes) {
|
|
507
|
+
atlasArgs.push("--exclude", exc);
|
|
464
508
|
}
|
|
465
509
|
}
|
|
466
510
|
|
|
467
511
|
try {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
reject: false
|
|
479
|
-
});
|
|
512
|
+
await execa(atlasBin, atlasArgs, {
|
|
513
|
+
cwd: process.cwd(),
|
|
514
|
+
stdio: "inherit",
|
|
515
|
+
env
|
|
516
|
+
});
|
|
517
|
+
} catch (err: unknown) {
|
|
518
|
+
logger.error(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
480
522
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
523
|
+
async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
|
|
524
|
+
const argsList = arg(
|
|
525
|
+
{
|
|
526
|
+
"--collections": String,
|
|
527
|
+
"--output": String,
|
|
528
|
+
"-c": "--collections",
|
|
529
|
+
"-o": "--output"
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
argv: rawArgs.slice(2),
|
|
533
|
+
permissive: true
|
|
534
|
+
}
|
|
535
|
+
);
|
|
484
536
|
|
|
485
|
-
|
|
537
|
+
const ddlScript = path.join(__cliDirname, "schema", "generate-postgres-ddl.ts");
|
|
538
|
+
const tsxBin = resolveLocalBin("tsx");
|
|
539
|
+
if (!tsxBin) {
|
|
540
|
+
logger.error(chalk.red("✗ Could not find tsx binary."));
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
486
543
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const stdout = stripAnsi(result.stdout || "").trim();
|
|
490
|
-
const stderr = stripAnsi(result.stderr || "").trim();
|
|
544
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
545
|
+
const outputPath = argsList["--output"] || path.join("drizzle", "schema.sql");
|
|
491
546
|
|
|
492
|
-
|
|
493
|
-
|
|
547
|
+
const cmdParts = [
|
|
548
|
+
tsxBin,
|
|
549
|
+
ddlScript,
|
|
550
|
+
`--collections=${collectionsPath}`,
|
|
551
|
+
`--output=${outputPath}`
|
|
552
|
+
];
|
|
494
553
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const errorOutput = stderr || stdout;
|
|
502
|
-
if (errorOutput) {
|
|
503
|
-
const lines = errorOutput.split("\n").filter((l: string) => l.trim());
|
|
504
|
-
let printedCount = 0;
|
|
505
|
-
for (const line of lines) {
|
|
506
|
-
if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates") || line.includes("permission denied")) {
|
|
507
|
-
logger.error(chalk.red(` ${line.trim()}`));
|
|
508
|
-
printedCount++;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
if (printedCount === 0) {
|
|
512
|
-
lines.slice(0, 10).forEach(line => logger.error(chalk.red(` ${line.trim()}`)));
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
logger.error("");
|
|
517
|
-
process.exit(1);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
554
|
+
try {
|
|
555
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
556
|
+
cwd: process.cwd(),
|
|
557
|
+
stdio: "inherit",
|
|
558
|
+
env: { ...process.env as Record<string, string> }
|
|
559
|
+
});
|
|
520
560
|
} catch (err: unknown) {
|
|
521
|
-
|
|
522
|
-
// eslint-disable-next-line no-control-regex
|
|
523
|
-
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\[?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣷⣯⣟⡿⢿⣻⣽]+\]\s*/g, "");
|
|
524
|
-
const cleaned = stripAnsi(msg).trim();
|
|
525
|
-
const hasTtyError = cleaned.includes("Interactive prompts require a TTY terminal");
|
|
526
|
-
logger.error(chalk.red(`\n✗ drizzle-kit ${action} failed.\n`));
|
|
527
|
-
if (hasTtyError) {
|
|
528
|
-
logger.error(chalk.red(" Error: Interactive prompts require a TTY terminal."));
|
|
529
|
-
logger.error(chalk.gray(" Please run with --force to skip interactive prompts or run in an interactive terminal."));
|
|
530
|
-
} else {
|
|
531
|
-
const lines = cleaned.split("\n").filter((l: string) => l.trim());
|
|
532
|
-
for (const line of lines) {
|
|
533
|
-
if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates")) {
|
|
534
|
-
logger.error(chalk.red(` ${line.trim()}`));
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (lines.length === 0) {
|
|
538
|
-
logger.error(chalk.gray(` ${cleaned}`));
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
logger.error("");
|
|
561
|
+
logger.error(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
542
562
|
process.exit(1);
|
|
543
563
|
}
|
|
544
564
|
}
|
|
@@ -561,8 +581,8 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
561
581
|
);
|
|
562
582
|
|
|
563
583
|
// Here we just invoke the local generate-drizzle-schema.ts since we are inside the postgresql-backend
|
|
564
|
-
// If installed in node_modules,
|
|
565
|
-
const generatorScript = path.join(
|
|
584
|
+
// If installed in node_modules, __cliDirname is node_modules/@rebasepro/server-postgresql/dist or src.
|
|
585
|
+
const generatorScript = path.join(__cliDirname, "schema", "generate-drizzle-schema.ts");
|
|
566
586
|
if (!fs.existsSync(generatorScript)) {
|
|
567
587
|
logger.error(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
|
|
568
588
|
process.exit(1);
|
|
@@ -619,7 +639,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
619
639
|
}
|
|
620
640
|
);
|
|
621
641
|
|
|
622
|
-
const introspectScript = path.join(
|
|
642
|
+
const introspectScript = path.join(__cliDirname, "schema", "introspect-db.ts");
|
|
623
643
|
if (!fs.existsSync(introspectScript)) {
|
|
624
644
|
logger.error(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
|
|
625
645
|
process.exit(1);
|
|
@@ -677,7 +697,7 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
|
|
|
677
697
|
}
|
|
678
698
|
);
|
|
679
699
|
|
|
680
|
-
const doctorScript = path.join(
|
|
700
|
+
const doctorScript = path.join(__cliDirname, "schema", "doctor-cli.ts");
|
|
681
701
|
if (!fs.existsSync(doctorScript)) {
|
|
682
702
|
logger.error(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
|
|
683
703
|
process.exit(1);
|
|
@@ -714,8 +734,7 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
|
|
|
714
734
|
|
|
715
735
|
|
|
716
736
|
// Entry point when called directly
|
|
717
|
-
|
|
718
|
-
const argv1Real = process.argv[1] ? fsSync.realpathSync(process.argv[1]) : "";
|
|
737
|
+
const argv1Real = process.argv[1] ? fs.realpathSync(process.argv[1]) : "";
|
|
719
738
|
if (import.meta.url === `file://${argv1Real}`) {
|
|
720
739
|
// Drop node and script path
|
|
721
740
|
runPluginCommand(process.argv.slice(2)).catch(() => process.exit(1));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CollectionRegistry } from "@rebasepro/common";
|
|
2
|
-
import { type
|
|
2
|
+
import { type EntityCollection, type Relation, getDataSourceCapabilities } from "@rebasepro/types";
|
|
3
3
|
import { PgEnum, PgTable } from "drizzle-orm/pg-core";
|
|
4
4
|
import { Relations } from "drizzle-orm";
|
|
5
5
|
import { CollectionRegistryInterface } from "../interfaces";
|
|
@@ -40,11 +40,11 @@ export class PostgresCollectionRegistry extends CollectionRegistry implements Co
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Finds collections assigned to a specific
|
|
43
|
+
* Finds collections assigned to a specific data source that do not have a registered table.
|
|
44
44
|
*/
|
|
45
|
-
getCollectionsWithoutTables(
|
|
45
|
+
getCollectionsWithoutTables(dataSourceKey = "(default)"): EntityCollection[] {
|
|
46
46
|
const collections = this.getCollections().filter(
|
|
47
|
-
c => c.
|
|
47
|
+
c => c.dataSource === dataSourceKey || (!c.dataSource && dataSourceKey === "(default)")
|
|
48
48
|
);
|
|
49
49
|
return collections.filter(c => !this.tables.has(getTableName(c)));
|
|
50
50
|
}
|
|
@@ -95,8 +95,8 @@ export class PostgresCollectionRegistry extends CollectionRegistry implements Co
|
|
|
95
95
|
*/
|
|
96
96
|
getRelationKeysForCollection(collectionPath: string): string[] {
|
|
97
97
|
const collection = this.getCollectionByPath(collectionPath);
|
|
98
|
-
if (!collection || !getDataSourceCapabilities(collection.
|
|
99
|
-
return
|
|
98
|
+
if (!collection || !getDataSourceCapabilities(collection.engine).supportsRelations || !collection.relations) return [];
|
|
99
|
+
return collection.relations!.map((r: Relation) => r.relationName || r.localKey || "").filter(Boolean);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
}
|