supatool 0.4.1 → 0.4.2

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.
@@ -33,6 +33,19 @@ Common Options:
33
33
  --config <path> Configuration file path
34
34
  -f, --force Force overwrite
35
35
 
36
+ seed command:
37
+ supatool seed -c <connection> [-t tables.yaml] [-o supabase/seeds]
38
+
39
+ tables.yaml format (schema-grouped):
40
+ public:
41
+ - users
42
+ - posts
43
+ admin:
44
+ - platforms
45
+
46
+ Output: supabase/seeds/<timestamp>/<schema>/<table>_seed.json
47
+ supabase/seeds/llms.txt (index for AI)
48
+
36
49
  For details, see the documentation.
37
50
  `;
38
51
  // Model Schema Usage
@@ -1041,7 +1041,12 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
1041
1041
  list.push(t.ddl);
1042
1042
  triggersByCategory.set(t.category, list);
1043
1043
  }
1044
- // Build merged DDL (table/view + RLS + triggers)
1044
+ // schema.table -> RLS status (for appending comment/DDL when no policies)
1045
+ const rlsStatusByCategory = new Map();
1046
+ for (const s of tableRlsStatus) {
1047
+ rlsStatusByCategory.set(`${s.schema}.${s.table}`, s);
1048
+ }
1049
+ // Build merged DDL (table/view + RLS + triggers). Tables with RLS disabled or 0 policies get a comment block in the file.
1045
1050
  const mergeRlsAndTriggers = (def) => {
1046
1051
  const cat = def.schema && def.name ? `${def.schema}.${def.name}` : def.category ?? '';
1047
1052
  let ddl = def.ddl.trimEnd();
@@ -1049,6 +1054,19 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
1049
1054
  if (rlsDdl) {
1050
1055
  ddl += '\n\n' + rlsDdl.trim();
1051
1056
  }
1057
+ else if (def.type === 'table' && def.schema && def.name) {
1058
+ const rlsStatus = rlsStatusByCategory.get(cat);
1059
+ if (rlsStatus) {
1060
+ if (!rlsStatus.rlsEnabled) {
1061
+ ddl += '\n\n-- RLS: disabled. Consider enabling for production.';
1062
+ ddl += '\n-- ALTER TABLE ' + def.schema + '.' + def.name + ' ENABLE ROW LEVEL SECURITY;';
1063
+ }
1064
+ else if (rlsStatus.policyCount === 0) {
1065
+ ddl += '\n\n-- RLS: enabled, no policies defined';
1066
+ ddl += '\nALTER TABLE ' + def.schema + '.' + def.name + ' ENABLE ROW LEVEL SECURITY;';
1067
+ }
1068
+ }
1069
+ }
1052
1070
  const trgList = triggersByCategory.get(cat);
1053
1071
  if (trgList && trgList.length > 0) {
1054
1072
  ddl += '\n\n' + trgList.map(t => t.trim()).join('\n\n');
@@ -8,17 +8,55 @@ const pg_1 = require("pg");
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const js_yaml_1 = __importDefault(require("js-yaml"));
11
+ /**
12
+ * Parse tables.yaml into { schema -> table[] } map.
13
+ * Format:
14
+ * public:
15
+ * - users
16
+ * - posts
17
+ * admin:
18
+ * - platforms
19
+ */
20
+ function parseTablesYaml(yamlPath) {
21
+ const yamlObj = js_yaml_1.default.load(fs_1.default.readFileSync(yamlPath, 'utf8'));
22
+ if (!yamlObj || typeof yamlObj !== 'object' || Array.isArray(yamlObj)) {
23
+ throw new Error('Invalid tables.yaml format. Use schema-grouped format:\n public:\n - users\n admin:\n - platforms');
24
+ }
25
+ // Detect old format: top-level "tables:" key with array value
26
+ if ('tables' in yamlObj && Array.isArray(yamlObj.tables)) {
27
+ throw new Error('Outdated tables.yaml format detected.\n\n' +
28
+ 'Migrate to schema-grouped format:\n\n' +
29
+ ' Before: After:\n' +
30
+ ' tables: public:\n' +
31
+ ' - users → - users\n' +
32
+ ' - posts - posts\n' +
33
+ ' - admin.x admin:\n' +
34
+ ' - x\n');
35
+ }
36
+ const entries = [];
37
+ for (const [schema, tables] of Object.entries(yamlObj)) {
38
+ if (!Array.isArray(tables)) {
39
+ throw new Error(`tables.yaml: value of "${schema}" must be a list of table names`);
40
+ }
41
+ for (const table of tables) {
42
+ // Detect dot notation inside a schema group
43
+ if (table.includes('.')) {
44
+ throw new Error(`tables.yaml: "${table}" under "${schema}" uses dot notation.\n` +
45
+ `Use schema-grouped format instead:\n` +
46
+ ` ${table.split('.')[0]}:\n` +
47
+ ` - ${table.split('.')[1]}`);
48
+ }
49
+ entries.push({ schema, table });
50
+ }
51
+ }
52
+ return entries;
53
+ }
11
54
  /**
12
55
  * Fetch table data from remote DB and generate AI seed JSON
13
56
  * @param options SeedGenOptions
14
57
  */
15
58
  async function generateSeedsFromRemote(options) {
16
- // Load tables.yaml
17
- const yamlObj = js_yaml_1.default.load(fs_1.default.readFileSync(options.tablesYamlPath, 'utf8'));
18
- if (!yamlObj || !Array.isArray(yamlObj.tables)) {
19
- throw new Error('Invalid tables.yaml format. Specify as tables: [ ... ]');
20
- }
21
- const tables = yamlObj.tables;
59
+ const tables = parseTablesYaml(options.tablesYamlPath);
22
60
  // Generate datetime subdir name (e.g. 20250705_1116_supatool)
23
61
  const now = new Date();
24
62
  const y = now.getFullYear();
@@ -28,67 +66,54 @@ async function generateSeedsFromRemote(options) {
28
66
  const mm = String(now.getMinutes()).padStart(2, '0');
29
67
  const folderName = `${y}${m}${d}_${hh}${mm}_supatool`;
30
68
  const outDir = path_1.default.join(options.outputDir, folderName);
31
- // Create output directory
32
- if (!fs_1.default.existsSync(outDir)) {
33
- fs_1.default.mkdirSync(outDir, { recursive: true });
34
- }
35
69
  // DB connection
36
70
  const client = new pg_1.Client({ connectionString: options.connectionString });
37
71
  await client.connect();
38
- let processedCount = 0;
39
- for (const tableFullName of tables) {
40
- // No schema specified -> public
41
- let schema = 'public';
42
- let table = tableFullName;
43
- if (tableFullName.includes('.')) {
44
- [schema, table] = tableFullName.split('.');
72
+ const processedFiles = [];
73
+ for (const { schema, table } of tables) {
74
+ // Create schema subdir
75
+ const schemaDir = path_1.default.join(outDir, schema);
76
+ if (!fs_1.default.existsSync(schemaDir)) {
77
+ fs_1.default.mkdirSync(schemaDir, { recursive: true });
45
78
  }
46
79
  // Fetch data
47
80
  const res = await client.query(`SELECT * FROM "${schema}"."${table}"`);
48
81
  const rows = res.rows;
49
- // File name
50
- const fileName = `${table}_seed.json`;
51
- const filePath = path_1.default.join(outDir, fileName);
52
82
  // Output JSON
83
+ const fileName = `${table}_seed.json`;
84
+ const filePath = path_1.default.join(schemaDir, fileName);
53
85
  const json = {
54
86
  table: `${schema}.${table}`,
55
87
  fetched_at: now.toISOString(),
56
- fetched_by: 'supatool v0.3.5',
88
+ fetched_by: 'supatool',
57
89
  note: 'This data is a snapshot of the remote DB at the above time. For AI coding reference. You can update it by running the update command again.',
58
90
  rows
59
91
  };
60
92
  fs_1.default.writeFileSync(filePath, JSON.stringify(json, null, 2), 'utf8');
61
- processedCount++;
93
+ processedFiles.push({ schema, table, fileName, rowCount: rows.length });
62
94
  }
63
95
  await client.end();
64
- // llms.txt index output (overwrite under supabase/seeds each run)
65
- const files = fs_1.default.readdirSync(outDir);
66
- const seedFiles = files.filter(f => f.endsWith('_seed.json'));
96
+ // llms.txt index (schema/file paths, overwrite each run)
67
97
  let llmsTxt = `# AI seed data index (generated by supatool)\n`;
68
98
  llmsTxt += `# fetched_at: ${now.toISOString()}\n`;
69
99
  llmsTxt += `# folder: ${folderName}\n`;
70
100
  llmsTxt += `# Schema catalog: ../schemas/llms.txt\n`;
71
- for (const basename of seedFiles) {
72
- const file = path_1.default.join(outDir, basename);
73
- const content = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
74
- // Table comment (empty if none)
101
+ for (const { schema, table, fileName, rowCount } of processedFiles) {
75
102
  let tableComment = '';
76
103
  try {
77
- const [schema, table] = content.table.split('.');
78
104
  const commentRes = await getTableComment(options.connectionString, schema, table);
79
105
  if (commentRes)
80
106
  tableComment = commentRes;
81
107
  }
82
108
  catch { }
83
- llmsTxt += `${content.table}: ${basename} (${Array.isArray(content.rows) ? content.rows.length : 0} rows)`;
109
+ llmsTxt += `${schema}.${table}: ${schema}/${fileName} (${rowCount} rows)`;
84
110
  if (tableComment)
85
111
  llmsTxt += ` # ${tableComment}`;
86
112
  llmsTxt += `\n`;
87
113
  }
88
114
  const llmsPath = path_1.default.join(options.outputDir, 'llms.txt');
89
115
  fs_1.default.writeFileSync(llmsPath, llmsTxt, 'utf8');
90
- // Output summary in English
91
- console.log(`Seed export completed. Processed tables: ${processedCount}`);
116
+ console.log(`Seed export completed. Processed tables: ${processedFiles.length}`);
92
117
  console.log(`llms.txt index written to: ${llmsPath}`);
93
118
  }
94
119
  /** Utility to get table comment */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supatool",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "CLI for Supabase: extract schema (tables, views, RLS, RPC) to files + llms.txt for LLM, deploy local schema, seed export. CRUD code gen deprecated.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",