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.
- package/dist/bin/helptext.js +13 -0
- package/dist/sync/definitionExtractor.js +19 -1
- package/dist/sync/seedGenerator.js +58 -33
- package/package.json +1 -1
package/dist/bin/helptext.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
for (const
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
93
|
+
processedFiles.push({ schema, table, fileName, rowCount: rows.length });
|
|
62
94
|
}
|
|
63
95
|
await client.end();
|
|
64
|
-
// llms.txt index
|
|
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
|
|
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 += `${
|
|
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
|
-
|
|
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.
|
|
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",
|