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 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 Clear output dir before writing
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
@@ -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 (use with --all-schemas)')
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
- await fsPromises.writeFile(filePath, headerComment + ddlWithNewline);
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: when --all-schemas, fetch all from DB and subtract excludeSchemas
1469
- if (useAllSchemas) {
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.0",
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": {