supatool 0.6.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 +12 -2
- package/dist/bin/supatool.js +2 -1
- package/dist/sync/definitionExtractor.js +33 -10
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -89,9 +89,19 @@ Pull schema from remote DB into local files:
|
|
|
89
89
|
```bash
|
|
90
90
|
supatool extract --all -o db/schemas
|
|
91
91
|
# Options:
|
|
92
|
-
# --schema public,agent Specify schemas
|
|
92
|
+
# --schema public,agent Specify schemas (explicit list)
|
|
93
|
+
# -e auth,storage Exclude schemas — targets all others automatically
|
|
93
94
|
# -t "user_*" Filter tables by pattern
|
|
94
|
-
# --force
|
|
95
|
+
# --force Delete .sql files for objects removed from DB
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Unchanged `.sql` files are never overwritten (content is compared excluding the generated header line). Use `--force` to also clean up `.sql` files whose corresponding DB objects have been dropped.
|
|
99
|
+
|
|
100
|
+
When you have many schemas and only want to exclude a few, use `-e` without `--schema`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Extract everything except auth and storage schemas
|
|
104
|
+
supatool extract --all -e auth,storage -o db/schemas
|
|
95
105
|
```
|
|
96
106
|
|
|
97
107
|
### Deploy
|
package/dist/bin/supatool.js
CHANGED
|
@@ -76,7 +76,7 @@ program
|
|
|
76
76
|
.option('--no-separate', 'Output all objects in same directory')
|
|
77
77
|
.option('--schema <schemas>', 'Target schemas, comma-separated (default: public)')
|
|
78
78
|
.option('--all-schemas', 'Target all schemas in the DB (use with -e to exclude some)')
|
|
79
|
-
.option('-e, --exclude-schema <schemas>', 'Schemas to exclude, comma-separated
|
|
79
|
+
.option('-e, --exclude-schema <schemas>', 'Schemas to exclude, comma-separated. Without --schema, targets all schemas automatically.')
|
|
80
80
|
.option('--config <path>', 'Configuration file path')
|
|
81
81
|
.option('-f, --force', 'Force overwrite without confirmation')
|
|
82
82
|
.action(async (options) => {
|
|
@@ -105,6 +105,7 @@ program
|
|
|
105
105
|
schemas: schemas,
|
|
106
106
|
allSchemas: options.allSchemas || false,
|
|
107
107
|
excludeSchemas,
|
|
108
|
+
schemasExplicit: !!options.schema,
|
|
108
109
|
version: package_json_1.version
|
|
109
110
|
});
|
|
110
111
|
}
|
|
@@ -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,7 @@ 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: schemasOption = ['public'], excludeSchemas = [], allSchemas: useAllSchemas = false, 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;
|
|
1348
1374
|
// schemas will be resolved after DB connect when useAllSchemas is true
|
|
1349
1375
|
let schemas = schemasOption;
|
|
1350
1376
|
// Disable Node.js SSL certificate verification
|
|
@@ -1465,8 +1491,9 @@ async function extractDefinitions(options) {
|
|
|
1465
1491
|
console.log(` Connection string length: ${encodedConnectionString.length}`);
|
|
1466
1492
|
await client.connect();
|
|
1467
1493
|
spinner.text = 'Connected to database';
|
|
1468
|
-
// Resolve schemas:
|
|
1469
|
-
|
|
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) {
|
|
1470
1497
|
const SYSTEM_SCHEMAS = ['information_schema', 'pg_catalog', 'pg_toast', 'pg_temp_1', 'pg_toast_temp_1'];
|
|
1471
1498
|
const discovered = await fetchAllSchemas(client);
|
|
1472
1499
|
schemas = discovered.filter(s => !SYSTEM_SCHEMAS.includes(s) && !excludeSchemas.includes(s));
|
|
@@ -1624,13 +1651,9 @@ async function extractDefinitions(options) {
|
|
|
1624
1651
|
console.warn('RLS status fetch skipped:', err);
|
|
1625
1652
|
}
|
|
1626
1653
|
}
|
|
1627
|
-
// When force: remove output dir then write (so removed tables don't leave files)
|
|
1628
|
-
if (force && fs.existsSync(outputDir)) {
|
|
1629
|
-
fs.rmSync(outputDir, { recursive: true });
|
|
1630
|
-
}
|
|
1631
1654
|
// Save definitions (table+RLS+triggers merged, schema folders)
|
|
1632
1655
|
spinner.text = 'Saving definitions to files...';
|
|
1633
|
-
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus);
|
|
1656
|
+
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus, force);
|
|
1634
1657
|
// Warn at extract time when any table has RLS disabled
|
|
1635
1658
|
const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
|
|
1636
1659
|
if (rlsNotEnabled.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supatool",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
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",
|
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
"database",
|
|
28
28
|
"migration"
|
|
29
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
|
+
},
|
|
30
38
|
"author": "IdeaGarage",
|
|
31
39
|
"license": "MIT",
|
|
32
40
|
"dependencies": {
|