supatool 0.4.1 → 0.4.3
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 +57 -17
- 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
|
|
@@ -355,6 +355,26 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
355
355
|
timestamp: Math.floor(Date.now() / 1000)
|
|
356
356
|
});
|
|
357
357
|
}
|
|
358
|
+
// Detect overloaded functions (same schema.name, different signatures)
|
|
359
|
+
const nameCount = new Map();
|
|
360
|
+
for (const row of result.rows) {
|
|
361
|
+
const key = `${row.schema_name}.${row.name}`;
|
|
362
|
+
const sig = `${row.name}(${row.identity_args || ''})`;
|
|
363
|
+
if (!nameCount.has(key))
|
|
364
|
+
nameCount.set(key, []);
|
|
365
|
+
nameCount.get(key).push(sig);
|
|
366
|
+
}
|
|
367
|
+
const overloads = [...nameCount.entries()].filter(([, sigs]) => sigs.length > 1);
|
|
368
|
+
if (overloads.length > 0) {
|
|
369
|
+
console.warn('\n⚠ Overloaded RPC functions detected (same name, different signatures):');
|
|
370
|
+
for (const [key, sigs] of overloads) {
|
|
371
|
+
console.warn(` ${key}`);
|
|
372
|
+
for (const sig of sigs) {
|
|
373
|
+
console.warn(` - ${sig}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
console.warn(' Note: Only the last definition will be written to the output file.\n');
|
|
377
|
+
}
|
|
358
378
|
return functions;
|
|
359
379
|
}
|
|
360
380
|
/**
|
|
@@ -843,19 +863,21 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
843
863
|
const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult, foreignKeyResult] = await Promise.all([
|
|
844
864
|
// Get column info
|
|
845
865
|
client.query(`
|
|
846
|
-
SELECT
|
|
866
|
+
SELECT
|
|
847
867
|
c.column_name,
|
|
848
868
|
c.data_type,
|
|
849
869
|
c.udt_name,
|
|
850
870
|
c.character_maximum_length,
|
|
851
871
|
c.is_nullable,
|
|
852
872
|
c.column_default,
|
|
873
|
+
c.is_generated,
|
|
874
|
+
c.generation_expression,
|
|
853
875
|
pg_catalog.format_type(a.atttypid, a.atttypmod) AS full_type
|
|
854
876
|
FROM information_schema.columns c
|
|
855
877
|
JOIN pg_class cl ON cl.relname = c.table_name
|
|
856
878
|
JOIN pg_namespace ns ON ns.nspname = c.table_schema AND ns.oid = cl.relnamespace
|
|
857
879
|
JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attname = c.column_name
|
|
858
|
-
WHERE c.table_schema = $1
|
|
880
|
+
WHERE c.table_schema = $1
|
|
859
881
|
AND c.table_name = $2
|
|
860
882
|
ORDER BY c.ordinal_position
|
|
861
883
|
`, [schemaName, tableName]),
|
|
@@ -951,13 +973,19 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
951
973
|
if (col.character_maximum_length) {
|
|
952
974
|
colDef += `(${col.character_maximum_length})`;
|
|
953
975
|
}
|
|
954
|
-
//
|
|
955
|
-
if (col.
|
|
956
|
-
colDef +=
|
|
976
|
+
// Generated column
|
|
977
|
+
if (col.is_generated === 'ALWAYS') {
|
|
978
|
+
colDef += ` GENERATED ALWAYS AS (${col.generation_expression}) STORED`;
|
|
957
979
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
980
|
+
else {
|
|
981
|
+
// NOT NULL constraint
|
|
982
|
+
if (col.is_nullable === 'NO') {
|
|
983
|
+
colDef += ' NOT NULL';
|
|
984
|
+
}
|
|
985
|
+
// Default value
|
|
986
|
+
if (col.column_default) {
|
|
987
|
+
colDef += ` DEFAULT ${col.column_default}`;
|
|
988
|
+
}
|
|
961
989
|
}
|
|
962
990
|
columnDefs.push(colDef);
|
|
963
991
|
}
|
|
@@ -1041,7 +1069,12 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
|
|
|
1041
1069
|
list.push(t.ddl);
|
|
1042
1070
|
triggersByCategory.set(t.category, list);
|
|
1043
1071
|
}
|
|
1044
|
-
//
|
|
1072
|
+
// schema.table -> RLS status (for appending comment/DDL when no policies)
|
|
1073
|
+
const rlsStatusByCategory = new Map();
|
|
1074
|
+
for (const s of tableRlsStatus) {
|
|
1075
|
+
rlsStatusByCategory.set(`${s.schema}.${s.table}`, s);
|
|
1076
|
+
}
|
|
1077
|
+
// Build merged DDL (table/view + RLS + triggers). Tables with RLS disabled or 0 policies get a comment block in the file.
|
|
1045
1078
|
const mergeRlsAndTriggers = (def) => {
|
|
1046
1079
|
const cat = def.schema && def.name ? `${def.schema}.${def.name}` : def.category ?? '';
|
|
1047
1080
|
let ddl = def.ddl.trimEnd();
|
|
@@ -1049,6 +1082,19 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
|
|
|
1049
1082
|
if (rlsDdl) {
|
|
1050
1083
|
ddl += '\n\n' + rlsDdl.trim();
|
|
1051
1084
|
}
|
|
1085
|
+
else if (def.type === 'table' && def.schema && def.name) {
|
|
1086
|
+
const rlsStatus = rlsStatusByCategory.get(cat);
|
|
1087
|
+
if (rlsStatus) {
|
|
1088
|
+
if (!rlsStatus.rlsEnabled) {
|
|
1089
|
+
ddl += '\n\n-- RLS: disabled. Consider enabling for production.';
|
|
1090
|
+
ddl += '\n-- ALTER TABLE ' + def.schema + '.' + def.name + ' ENABLE ROW LEVEL SECURITY;';
|
|
1091
|
+
}
|
|
1092
|
+
else if (rlsStatus.policyCount === 0) {
|
|
1093
|
+
ddl += '\n\n-- RLS: enabled, no policies defined';
|
|
1094
|
+
ddl += '\nALTER TABLE ' + def.schema + '.' + def.name + ' ENABLE ROW LEVEL SECURITY;';
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1052
1098
|
const trgList = triggersByCategory.get(cat);
|
|
1053
1099
|
if (trgList && trgList.length > 0) {
|
|
1054
1100
|
ddl += '\n\n' + trgList.map(t => t.trim()).join('\n\n');
|
|
@@ -1091,13 +1137,7 @@ async function saveDefinitionsByType(definitions, outputDir, separateDirectories
|
|
|
1091
1137
|
if (!fs.existsSync(targetDir)) {
|
|
1092
1138
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
1093
1139
|
}
|
|
1094
|
-
|
|
1095
|
-
if (def.type === 'function') {
|
|
1096
|
-
fileName = `fn_${def.name}.sql`;
|
|
1097
|
-
}
|
|
1098
|
-
else {
|
|
1099
|
-
fileName = `${def.name}.sql`;
|
|
1100
|
-
}
|
|
1140
|
+
const fileName = `${def.name}.sql`;
|
|
1101
1141
|
const filePath = path.join(targetDir, fileName);
|
|
1102
1142
|
const ddlWithNewline = def.ddl.endsWith('\n') ? def.ddl : def.ddl + '\n';
|
|
1103
1143
|
await fsPromises.writeFile(filePath, headerComment + ddlWithNewline);
|
|
@@ -1157,7 +1197,7 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
|
|
|
1157
1197
|
// Build relative path per file (schema/type/file when multiSchema)
|
|
1158
1198
|
const getRelPath = (def) => {
|
|
1159
1199
|
const typeDir = separateDirectories ? (typeDirNames[def.type] ?? def.type) : '.';
|
|
1160
|
-
const fileName =
|
|
1200
|
+
const fileName = `${def.name}.sql`;
|
|
1161
1201
|
if (multiSchema && def.schema) {
|
|
1162
1202
|
return `${def.schema}/${typeDir}/${fileName}`;
|
|
1163
1203
|
}
|
|
@@ -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.3",
|
|
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",
|