supatool 0.5.0 → 0.6.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/README.md +97 -23
- package/dist/bin/helptext.js +40 -31
- package/dist/bin/supatool.js +71 -149
- package/dist/sync/config.js +8 -7
- package/dist/sync/definitionExtractor.js +44 -8
- package/dist/sync/fetchRemoteSchemas.js +4 -2
- package/dist/sync/generateMigration.js +5 -5
- package/dist/sync/migrateRemote.js +114 -0
- package/dist/sync/seedGenerator.js +15 -4
- package/package.json +15 -6
- package/dist/integrations/supabase/crud-autogen/tasks.js +0 -220
- package/dist/integrations/supabase/crud-autogen/workflows.js +0 -220
package/dist/sync/config.js
CHANGED
|
@@ -73,12 +73,13 @@ function loadConfig(configPath) {
|
|
|
73
73
|
function resolveConfig(options, configPath) {
|
|
74
74
|
const fileConfig = loadConfig(configPath);
|
|
75
75
|
const connectionString = options.connectionString ||
|
|
76
|
+
process.env.DB_CONNECTION_STRING ||
|
|
76
77
|
process.env.SUPABASE_CONNECTION_STRING ||
|
|
77
78
|
process.env.DATABASE_URL ||
|
|
78
79
|
fileConfig.connectionString;
|
|
79
80
|
return {
|
|
80
81
|
connectionString,
|
|
81
|
-
schemaDir: options.schemaDir || fileConfig.schemaDir || './
|
|
82
|
+
schemaDir: options.schemaDir || fileConfig.schemaDir || './db/schemas',
|
|
82
83
|
tablePattern: options.tablePattern || fileConfig.tablePattern || '*',
|
|
83
84
|
migration: fileConfig.migration
|
|
84
85
|
};
|
|
@@ -88,12 +89,12 @@ function resolveConfig(options, configPath) {
|
|
|
88
89
|
*/
|
|
89
90
|
function createConfigTemplate(outputPath) {
|
|
90
91
|
const template = {
|
|
91
|
-
schemaDir: "./
|
|
92
|
+
schemaDir: "./db/schemas",
|
|
92
93
|
tablePattern: "*",
|
|
93
94
|
migration: {
|
|
94
95
|
naming: "timestamp",
|
|
95
96
|
"_naming_comment": "Use 'sequential' for NNN_description.sql format, 'timestamp' for YYYYMMDDHHMMSS_description.sql",
|
|
96
|
-
dir: "
|
|
97
|
+
dir: "db/migrations"
|
|
97
98
|
},
|
|
98
99
|
"_comment": "Set credentials in .env.local — never put secrets in this file."
|
|
99
100
|
};
|
|
@@ -111,12 +112,12 @@ function ensureEnvLocalTemplate() {
|
|
|
111
112
|
return;
|
|
112
113
|
const template = [
|
|
113
114
|
'# supatool credentials — never commit this file',
|
|
114
|
-
'#
|
|
115
|
-
'
|
|
116
|
-
'SUPABASE_SERVICE_ROLE_KEY=your-service-role-key',
|
|
115
|
+
'# PostgreSQL connection string (Cloud SQL, Supabase, or any PostgreSQL)',
|
|
116
|
+
'DB_CONNECTION_STRING=postgresql://user:password@host:port/database',
|
|
117
117
|
'',
|
|
118
|
-
'#
|
|
118
|
+
'# Legacy aliases (still accepted for backward compatibility)',
|
|
119
119
|
'# SUPABASE_CONNECTION_STRING=postgresql://user:password@host:port/database',
|
|
120
|
+
'# DATABASE_URL=postgresql://user:password@host:port/database',
|
|
120
121
|
].join('\n') + '\n';
|
|
121
122
|
fs.writeFileSync(envLocalPath, template, 'utf-8');
|
|
122
123
|
console.log('.env.local template created — fill in your credentials.');
|
|
@@ -1039,7 +1039,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
1039
1039
|
/**
|
|
1040
1040
|
* Save definitions to files (merge RLS/triggers into table/view; schema folders when multi-schema)
|
|
1041
1041
|
*/
|
|
1042
|
-
async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version, tableRlsStatus = []) {
|
|
1042
|
+
async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version, tableRlsStatus = [], force = false) {
|
|
1043
1043
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1044
1044
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1045
1045
|
const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
@@ -1128,6 +1128,7 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
|
|
|
1128
1128
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
1129
1129
|
}
|
|
1130
1130
|
const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
|
1131
|
+
const writtenPaths = new Set();
|
|
1131
1132
|
for (const def of toWrite) {
|
|
1132
1133
|
const typeDir = typeDirNames[def.type];
|
|
1133
1134
|
const baseTypeDir = separateDirectories ? typeDir : '.';
|
|
@@ -1140,7 +1141,32 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
|
|
|
1140
1141
|
const fileName = `${def.name}.sql`;
|
|
1141
1142
|
const filePath = path.join(targetDir, fileName);
|
|
1142
1143
|
const ddlWithNewline = def.ddl.endsWith('\n') ? def.ddl : def.ddl + '\n';
|
|
1143
|
-
|
|
1144
|
+
const newContent = headerComment + ddlWithNewline;
|
|
1145
|
+
writtenPaths.add(filePath);
|
|
1146
|
+
// Skip write if content unchanged (ignore header line which contains the date)
|
|
1147
|
+
if (fs.existsSync(filePath)) {
|
|
1148
|
+
const existingContent = await fsPromises.readFile(filePath, 'utf8');
|
|
1149
|
+
const stripHeader = (c) => c.split('\n').slice(1).join('\n');
|
|
1150
|
+
if (stripHeader(existingContent) === stripHeader(newContent)) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
await fsPromises.writeFile(filePath, newContent);
|
|
1155
|
+
}
|
|
1156
|
+
// When force: delete SQL files that no longer correspond to any extracted object
|
|
1157
|
+
if (force && fs.existsSync(outputDir)) {
|
|
1158
|
+
const deleteStaleSqlFiles = (dir) => {
|
|
1159
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1160
|
+
const fullPath = path.join(dir, entry.name);
|
|
1161
|
+
if (entry.isDirectory()) {
|
|
1162
|
+
deleteStaleSqlFiles(fullPath);
|
|
1163
|
+
}
|
|
1164
|
+
else if (entry.name.endsWith('.sql') && !writtenPaths.has(fullPath)) {
|
|
1165
|
+
fs.unlinkSync(fullPath);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
deleteStaleSqlFiles(outputDir);
|
|
1144
1170
|
}
|
|
1145
1171
|
await generateIndexFile(toWrite, outputDir, separateDirectories, multiSchema, relations, rpcTables, allSchemas, schemas, version, tableRlsStatus);
|
|
1146
1172
|
}
|
|
@@ -1344,7 +1370,9 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
|
|
|
1344
1370
|
* Classify and output definitions
|
|
1345
1371
|
*/
|
|
1346
1372
|
async function extractDefinitions(options) {
|
|
1347
|
-
const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'], version } = options;
|
|
1373
|
+
const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas: schemasOption = ['public'], excludeSchemas = [], allSchemas: useAllSchemas = false, schemasExplicit = false, version } = options;
|
|
1374
|
+
// schemas will be resolved after DB connect when useAllSchemas is true
|
|
1375
|
+
let schemas = schemasOption;
|
|
1348
1376
|
// Disable Node.js SSL certificate verification
|
|
1349
1377
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
1350
1378
|
// Connection string validation
|
|
@@ -1463,6 +1491,18 @@ async function extractDefinitions(options) {
|
|
|
1463
1491
|
console.log(` Connection string length: ${encodedConnectionString.length}`);
|
|
1464
1492
|
await client.connect();
|
|
1465
1493
|
spinner.text = 'Connected to database';
|
|
1494
|
+
// Resolve schemas: --all-schemas or -e alone (without explicit --schema) → fetch all from DB and subtract excludeSchemas
|
|
1495
|
+
const useAllSchemasEffective = useAllSchemas || (excludeSchemas.length > 0 && !schemasExplicit);
|
|
1496
|
+
if (useAllSchemasEffective) {
|
|
1497
|
+
const SYSTEM_SCHEMAS = ['information_schema', 'pg_catalog', 'pg_toast', 'pg_temp_1', 'pg_toast_temp_1'];
|
|
1498
|
+
const discovered = await fetchAllSchemas(client);
|
|
1499
|
+
schemas = discovered.filter(s => !SYSTEM_SCHEMAS.includes(s) && !excludeSchemas.includes(s));
|
|
1500
|
+
console.log(`Schemas (all minus excluded): ${schemas.join(', ')}`);
|
|
1501
|
+
}
|
|
1502
|
+
else if (excludeSchemas.length > 0) {
|
|
1503
|
+
schemas = schemas.filter(s => !excludeSchemas.includes(s));
|
|
1504
|
+
console.log(`Schemas (filtered): ${schemas.join(', ')}`);
|
|
1505
|
+
}
|
|
1466
1506
|
let allDefinitions = [];
|
|
1467
1507
|
// Initialize progress tracker
|
|
1468
1508
|
const progress = {
|
|
@@ -1611,13 +1651,9 @@ async function extractDefinitions(options) {
|
|
|
1611
1651
|
console.warn('RLS status fetch skipped:', err);
|
|
1612
1652
|
}
|
|
1613
1653
|
}
|
|
1614
|
-
// When force: remove output dir then write (so removed tables don't leave files)
|
|
1615
|
-
if (force && fs.existsSync(outputDir)) {
|
|
1616
|
-
fs.rmSync(outputDir, { recursive: true });
|
|
1617
|
-
}
|
|
1618
1654
|
// Save definitions (table+RLS+triggers merged, schema folders)
|
|
1619
1655
|
spinner.text = 'Saving definitions to files...';
|
|
1620
|
-
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus);
|
|
1656
|
+
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus, force);
|
|
1621
1657
|
// Warn at extract time when any table has RLS disabled
|
|
1622
1658
|
const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
|
|
1623
1659
|
if (rlsNotEnabled.length > 0) {
|
|
@@ -56,8 +56,10 @@ async function fetchRemoteSchemas(connectionString, targetTableNames) {
|
|
|
56
56
|
// console.log('Connecting to database...');
|
|
57
57
|
try {
|
|
58
58
|
// Basic connection string check
|
|
59
|
-
if (!connectionString ||
|
|
60
|
-
|
|
59
|
+
if (!connectionString ||
|
|
60
|
+
(!connectionString.startsWith('postgresql://') &&
|
|
61
|
+
!connectionString.startsWith('postgres://'))) {
|
|
62
|
+
throw new Error('Invalid connection string. Please specify a valid postgresql:// or postgres:// format.');
|
|
61
63
|
}
|
|
62
64
|
// Parse URL and display connection info
|
|
63
65
|
const url = new URL(connectionString);
|
|
@@ -140,7 +140,7 @@ function generateAlterStatements(tableName, fromDdl, toDdl) {
|
|
|
140
140
|
* Generate migration file
|
|
141
141
|
*/
|
|
142
142
|
async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.', migrationConfig) {
|
|
143
|
-
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? '
|
|
143
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
144
144
|
// Create migrations directory
|
|
145
145
|
if (!fs.existsSync(migrationDir)) {
|
|
146
146
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
@@ -209,7 +209,7 @@ function analyzeDiffForTemplate(fromDdl, toDdl) {
|
|
|
209
209
|
* Generate manual migration template
|
|
210
210
|
*/
|
|
211
211
|
async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir, migrationConfig) {
|
|
212
|
-
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? '
|
|
212
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
213
213
|
if (!fs.existsSync(migrationDir)) {
|
|
214
214
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
215
215
|
}
|
|
@@ -282,7 +282,7 @@ ${migrationStatements.join('\n')}
|
|
|
282
282
|
* (old exists on remote, new exists on local, columns are highly similar)
|
|
283
283
|
*/
|
|
284
284
|
async function generateRenameTableMigrationFile(schema, oldName, newName, projectDir = '.', migrationConfig) {
|
|
285
|
-
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? '
|
|
285
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
286
286
|
if (!fs.existsSync(migrationDir)) {
|
|
287
287
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
288
288
|
}
|
|
@@ -320,7 +320,7 @@ async function generateFunctionMigrationFile(schema, funcName, localDdl, remoteD
|
|
|
320
320
|
const normalizedRemote = normalizeFunctionDdl(remoteDdl);
|
|
321
321
|
if (normalizedLocal === normalizedRemote)
|
|
322
322
|
return null;
|
|
323
|
-
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? '
|
|
323
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
324
324
|
if (!fs.existsSync(migrationDir)) {
|
|
325
325
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
326
326
|
}
|
|
@@ -342,7 +342,7 @@ ${normalizedLocal.endsWith(';') ? normalizedLocal : normalizedLocal + ';'}
|
|
|
342
342
|
async function generateRlsMigrationFile(changedPolicies, droppedPolicies, projectDir = '.', migrationConfig) {
|
|
343
343
|
if (changedPolicies.length === 0 && droppedPolicies.length === 0)
|
|
344
344
|
return null;
|
|
345
|
-
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? '
|
|
345
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
346
346
|
if (!fs.existsSync(migrationDir)) {
|
|
347
347
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
348
348
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.migrateRemote = migrateRemote;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const pg_1 = require("pg");
|
|
40
|
+
const MIGRATIONS_TABLE = '_supatool_migrations';
|
|
41
|
+
/**
|
|
42
|
+
* Apply pending SQL migration files to remote DB.
|
|
43
|
+
*
|
|
44
|
+
* Tracks applied migrations in _supatool_migrations table.
|
|
45
|
+
* Files are applied in alphabetical order (timestamp or sequential naming).
|
|
46
|
+
*/
|
|
47
|
+
async function migrateRemote(options) {
|
|
48
|
+
const { connectionString, migrationsDir, dryRun = false } = options;
|
|
49
|
+
// Collect .sql files
|
|
50
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
51
|
+
console.error(`❌ Migrations directory not found: ${migrationsDir}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const allFiles = fs.readdirSync(migrationsDir)
|
|
55
|
+
.filter(f => f.endsWith('.sql'))
|
|
56
|
+
.sort();
|
|
57
|
+
if (allFiles.length === 0) {
|
|
58
|
+
console.log('No migration files found.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const client = new pg_1.Client({
|
|
62
|
+
connectionString,
|
|
63
|
+
ssl: { rejectUnauthorized: false }
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
await client.connect();
|
|
67
|
+
// Ensure tracking table exists
|
|
68
|
+
await client.query(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
70
|
+
id SERIAL PRIMARY KEY,
|
|
71
|
+
filename TEXT NOT NULL UNIQUE,
|
|
72
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
// Get already-applied migrations
|
|
76
|
+
const applied = await client.query(`SELECT filename FROM ${MIGRATIONS_TABLE} ORDER BY filename`);
|
|
77
|
+
const appliedSet = new Set(applied.rows.map(r => r.filename));
|
|
78
|
+
const pending = allFiles.filter(f => !appliedSet.has(f));
|
|
79
|
+
if (pending.length === 0) {
|
|
80
|
+
console.log('✅ All migrations already applied.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`Pending migrations: ${pending.length}`);
|
|
84
|
+
for (const f of pending) {
|
|
85
|
+
console.log(` • ${f}`);
|
|
86
|
+
}
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
console.log('\n(dry-run) No changes applied.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Apply each pending migration in a transaction
|
|
92
|
+
for (const filename of pending) {
|
|
93
|
+
const filepath = path.join(migrationsDir, filename);
|
|
94
|
+
const sql = fs.readFileSync(filepath, 'utf-8');
|
|
95
|
+
process.stdout.write(`Applying ${filename}... `);
|
|
96
|
+
await client.query('BEGIN');
|
|
97
|
+
try {
|
|
98
|
+
await client.query(sql);
|
|
99
|
+
await client.query(`INSERT INTO ${MIGRATIONS_TABLE} (filename) VALUES ($1)`, [filename]);
|
|
100
|
+
await client.query('COMMIT');
|
|
101
|
+
console.log('✅');
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
await client.query('ROLLBACK');
|
|
105
|
+
console.log('❌');
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
console.log(`\n✅ Applied ${pending.length} migration(s).`);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await client.end();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -56,6 +56,7 @@ function parseTablesYaml(yamlPath) {
|
|
|
56
56
|
* @param options SeedGenOptions
|
|
57
57
|
*/
|
|
58
58
|
async function generateSeedsFromRemote(options) {
|
|
59
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
59
60
|
const tables = parseTablesYaml(options.tablesYamlPath);
|
|
60
61
|
// Generate datetime subdir name (e.g. 20250705_1116_supatool)
|
|
61
62
|
const now = new Date();
|
|
@@ -67,7 +68,10 @@ async function generateSeedsFromRemote(options) {
|
|
|
67
68
|
const folderName = `${y}${m}${d}_${hh}${mm}_supatool`;
|
|
68
69
|
const outDir = path_1.default.join(options.outputDir, folderName);
|
|
69
70
|
// DB connection
|
|
70
|
-
const client = new pg_1.Client({
|
|
71
|
+
const client = new pg_1.Client({
|
|
72
|
+
connectionString: options.connectionString,
|
|
73
|
+
ssl: { rejectUnauthorized: false }
|
|
74
|
+
});
|
|
71
75
|
await client.connect();
|
|
72
76
|
const processedFiles = [];
|
|
73
77
|
for (const { schema, table } of tables) {
|
|
@@ -77,8 +81,15 @@ async function generateSeedsFromRemote(options) {
|
|
|
77
81
|
fs_1.default.mkdirSync(schemaDir, { recursive: true });
|
|
78
82
|
}
|
|
79
83
|
// Fetch data
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
let rows;
|
|
85
|
+
try {
|
|
86
|
+
const res = await client.query(`SELECT * FROM "${schema}"."${table}"`);
|
|
87
|
+
rows = res.rows;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.warn(`⚠️ Skip: ${schema}.${table} — ${err.message}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
82
93
|
// Output JSON
|
|
83
94
|
const fileName = `${table}_seed.json`;
|
|
84
95
|
const filePath = path_1.default.join(schemaDir, fileName);
|
|
@@ -118,7 +129,7 @@ async function generateSeedsFromRemote(options) {
|
|
|
118
129
|
}
|
|
119
130
|
/** Utility to get table comment */
|
|
120
131
|
async function getTableComment(connectionString, schema, table) {
|
|
121
|
-
const client = new pg_1.Client({ connectionString });
|
|
132
|
+
const client = new pg_1.Client({ connectionString, ssl: { rejectUnauthorized: false } });
|
|
122
133
|
await client.connect();
|
|
123
134
|
try {
|
|
124
135
|
const res = await client.query(`SELECT obj_description(c.oid) as comment FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'`, [table, schema]);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supatool",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for Supabase: extract schema
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "CLI for PostgreSQL (Cloud SQL / Supabase): extract schema to files, deploy schema diffs, apply migrations, seed export.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
|
+
"test": "npm run build && node test/smoke.js",
|
|
12
13
|
"start": "tsx src/bin/supatool.ts",
|
|
13
14
|
"local": "tsx src/bin/supatool.ts"
|
|
14
15
|
},
|
|
@@ -17,18 +18,26 @@
|
|
|
17
18
|
"bin"
|
|
18
19
|
],
|
|
19
20
|
"keywords": [
|
|
21
|
+
"postgresql",
|
|
22
|
+
"cloud-sql",
|
|
20
23
|
"supabase",
|
|
21
|
-
"crud",
|
|
22
24
|
"cli",
|
|
23
25
|
"typescript",
|
|
24
|
-
"React",
|
|
25
26
|
"postgres",
|
|
26
|
-
"database"
|
|
27
|
+
"database",
|
|
28
|
+
"migration"
|
|
27
29
|
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/idea-garage/supatool"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/idea-garage/supatool#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/idea-garage/supatool/issues"
|
|
37
|
+
},
|
|
28
38
|
"author": "IdeaGarage",
|
|
29
39
|
"license": "MIT",
|
|
30
40
|
"dependencies": {
|
|
31
|
-
"@supabase/supabase-js": "^2.49.4",
|
|
32
41
|
"commander": "^13.1.0",
|
|
33
42
|
"diff": "^5.2.0",
|
|
34
43
|
"dotenv": "^16.5.0",
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getTasksByFilters = getTasksByFilters;
|
|
4
|
-
exports.getTasksSingleByFilters = getTasksSingleByFilters;
|
|
5
|
-
exports.getTasksById = getTasksById;
|
|
6
|
-
exports.createTasks = createTasks;
|
|
7
|
-
exports.updateTasks = updateTasks;
|
|
8
|
-
exports.deleteTasks = deleteTasks;
|
|
9
|
-
exports.queryTasks = queryTasks;
|
|
10
|
-
// Supabase CRUD operations for tasks
|
|
11
|
-
// This file is automatically generated. Do not edit it directly.
|
|
12
|
-
const client_1 = require("../client");
|
|
13
|
-
// Function to apply filters to a query
|
|
14
|
-
function applyFilters(query, filters) {
|
|
15
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
16
|
-
if (Array.isArray(value)) {
|
|
17
|
-
query = query.in(key, value); // Use 'in' for array values
|
|
18
|
-
}
|
|
19
|
-
else if (typeof value === 'object' && value !== null) {
|
|
20
|
-
for (const [operator, val] of Object.entries(value)) {
|
|
21
|
-
switch (operator) {
|
|
22
|
-
case 'eq':
|
|
23
|
-
query = query.eq(key, val);
|
|
24
|
-
break;
|
|
25
|
-
case 'neq':
|
|
26
|
-
query = query.neq(key, val);
|
|
27
|
-
break;
|
|
28
|
-
case 'like':
|
|
29
|
-
query = query.like(key, val);
|
|
30
|
-
break;
|
|
31
|
-
case 'ilike':
|
|
32
|
-
query = query.ilike(key, val);
|
|
33
|
-
break;
|
|
34
|
-
case 'lt':
|
|
35
|
-
query = query.lt(key, val);
|
|
36
|
-
break;
|
|
37
|
-
case 'lte':
|
|
38
|
-
query = query.lte(key, val);
|
|
39
|
-
break;
|
|
40
|
-
case 'gte':
|
|
41
|
-
query = query.gte(key, val);
|
|
42
|
-
break;
|
|
43
|
-
case 'gt':
|
|
44
|
-
query = query.gt(key, val);
|
|
45
|
-
break;
|
|
46
|
-
case 'contains':
|
|
47
|
-
query = query.contains(key, val);
|
|
48
|
-
break;
|
|
49
|
-
case 'contains_any':
|
|
50
|
-
query = query.contains_any(key, val);
|
|
51
|
-
break;
|
|
52
|
-
case 'contains_all':
|
|
53
|
-
query = query.contains_all(key, val);
|
|
54
|
-
break;
|
|
55
|
-
// Add more operators as needed
|
|
56
|
-
default:
|
|
57
|
-
throw new Error('Unsupported operator: ' + operator);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
query = query.eq(key, value); // Default to 'eq' for simple values
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return query;
|
|
66
|
-
}
|
|
67
|
-
// Read multiple rows with dynamic filters
|
|
68
|
-
async function getTasksByFilters({ filters }) {
|
|
69
|
-
try {
|
|
70
|
-
let query = client_1.supabase.from('tasks').select('*');
|
|
71
|
-
query = applyFilters(query, filters);
|
|
72
|
-
const result = await query;
|
|
73
|
-
if (result.error) {
|
|
74
|
-
throw new Error(`Failed to fetch tasks: ${result.error.message}`);
|
|
75
|
-
}
|
|
76
|
-
return result.data || [];
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
console.error('Error in getTasksByFilters:', error);
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// Read a single row with dynamic filters
|
|
84
|
-
async function getTasksSingleByFilters({ filters }) {
|
|
85
|
-
try {
|
|
86
|
-
let query = client_1.supabase.from('tasks').select('*');
|
|
87
|
-
query = applyFilters(query, filters).single();
|
|
88
|
-
const result = await query;
|
|
89
|
-
if (result.error) {
|
|
90
|
-
if (result.error.code === 'PGRST116') {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
throw new Error(`Failed to fetch tasks: ${result.error.message}`);
|
|
94
|
-
}
|
|
95
|
-
return result.data;
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
console.error('Error in getTasksSingleByFilters:', error);
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// Read single row using id
|
|
103
|
-
async function getTasksById({ id }) {
|
|
104
|
-
if (!id) {
|
|
105
|
-
throw new Error('ID is required');
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const result = await client_1.supabase
|
|
109
|
-
.from('tasks')
|
|
110
|
-
.select('*')
|
|
111
|
-
.eq('id', id)
|
|
112
|
-
.single();
|
|
113
|
-
if (result.error) {
|
|
114
|
-
if (result.error.code === 'PGRST116') {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
throw new Error(`Failed to fetch tasks: ${result.error.message}`);
|
|
118
|
-
}
|
|
119
|
-
return result.data;
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
console.error('Error in getTasksById:', error);
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Create Function
|
|
127
|
-
async function createTasks({ data }) {
|
|
128
|
-
if (!data) {
|
|
129
|
-
throw new Error('Data is required for creation');
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
const result = await client_1.supabase
|
|
133
|
-
.from('tasks')
|
|
134
|
-
.insert([data])
|
|
135
|
-
.select()
|
|
136
|
-
.single();
|
|
137
|
-
if (result.error) {
|
|
138
|
-
throw new Error(`Failed to create tasks: ${result.error.message}`);
|
|
139
|
-
}
|
|
140
|
-
if (!result.data) {
|
|
141
|
-
throw new Error('No data returned after creation');
|
|
142
|
-
}
|
|
143
|
-
return result.data;
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
console.error('Error in createTasks:', error);
|
|
147
|
-
throw error;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// Update Function
|
|
151
|
-
async function updateTasks({ id, data }) {
|
|
152
|
-
if (!id) {
|
|
153
|
-
throw new Error('ID is required for update');
|
|
154
|
-
}
|
|
155
|
-
if (!data || Object.keys(data).length === 0) {
|
|
156
|
-
throw new Error('Update data is required');
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
const result = await client_1.supabase
|
|
160
|
-
.from('tasks')
|
|
161
|
-
.update(data)
|
|
162
|
-
.eq('id', id)
|
|
163
|
-
.select()
|
|
164
|
-
.single();
|
|
165
|
-
if (result.error) {
|
|
166
|
-
if (result.error.code === 'PGRST116') {
|
|
167
|
-
throw new Error(`tasks with ID ${id} not found`);
|
|
168
|
-
}
|
|
169
|
-
throw new Error(`Failed to update tasks: ${result.error.message}`);
|
|
170
|
-
}
|
|
171
|
-
if (!result.data) {
|
|
172
|
-
throw new Error(`tasks with ID ${id} not found`);
|
|
173
|
-
}
|
|
174
|
-
return result.data;
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
console.error('Error in updateTasks:', error);
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
// Delete Function
|
|
182
|
-
async function deleteTasks({ id }) {
|
|
183
|
-
if (!id) {
|
|
184
|
-
throw new Error('ID is required for deletion');
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
const result = await client_1.supabase
|
|
188
|
-
.from('tasks')
|
|
189
|
-
.delete()
|
|
190
|
-
.eq('id', id);
|
|
191
|
-
if (result.error) {
|
|
192
|
-
throw new Error(`Failed to delete tasks: ${result.error.message}`);
|
|
193
|
-
}
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
console.error('Error in deleteTasks:', error);
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// Custom query function
|
|
202
|
-
async function queryTasks({ query }) {
|
|
203
|
-
if (!query) {
|
|
204
|
-
throw new Error('Query is required');
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
const result = await client_1.supabase
|
|
208
|
-
.from('tasks')
|
|
209
|
-
.select(query);
|
|
210
|
-
if (result.error) {
|
|
211
|
-
throw new Error(`Failed to execute query: ${result.error.message}`);
|
|
212
|
-
}
|
|
213
|
-
return result.data || [];
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
console.error('Error in queryTasks:', error);
|
|
217
|
-
throw error;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
// All functions are exported individually above
|