supatool 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 IdeaGarage
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # Supatool
2
+
3
+ A CLI tool that automatically generates TypeScript CRUD code from Supabase type definitions.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm install -g supatool
9
+ # or
10
+ yarn global add supatool
11
+ # or
12
+ pnpm add -g supatool
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ 1. Generate Supabase type definition file
18
+
19
+ ```
20
+ npx supabase gen types typescript --project-id your_project_ref --schema public > shared/types.ts
21
+ ```
22
+
23
+ 2. Auto-generate CRUD code
24
+
25
+ ```
26
+ supatool
27
+ ```
28
+ - Output: `src/integrations/supabase/crud-autogen/`
29
+
30
+ 3. Subcommands
31
+
32
+ See: [src/bin/helptext.ts](./src/bin/helptext.ts)
33
+
34
+ For details on how to specify input/output folders, please refer to this as well.
35
+
36
+ ## VSCode/Cursor: Run Supabase CLI and supatool together
37
+
38
+ You can add a task to `.vscode/tasks.json` to run both commands at once:
39
+
40
+ ```json
41
+ {
42
+ "version": "2.0.0",
43
+ "tasks": [
44
+ {
45
+ "label": "Generate Supabase types and CRUD",
46
+ "type": "shell",
47
+ "command": "mkdir -p shared && npx supabase gen types typescript --project-id your_project_id --schema public > shared/types.ts && supatool crud --force",
48
+ "group": "build"
49
+ }
50
+ ]
51
+ }
52
+ ```
53
+
54
+ ## Example: How to use generated CRUD code in your app
55
+
56
+ CRUD utility files for the `apps` table will be generated in `src/integrations/supabase/crud-autogen/` by default.
57
+
58
+ You can import and use these functions in your application as follows:
59
+
60
+ ```ts
61
+ // Example: Using CRUD functions for the apps table
62
+ import {
63
+ selectAppsRowsWithFilters,
64
+ selectAppsSingleRowWithFilters,
65
+ selectAppsRowById,
66
+ insertAppsRow,
67
+ updateAppsRow,
68
+ deleteAppsRow,
69
+ } from 'src/integrations/supabase/crud-autogen/apps';
70
+
71
+ // Get multiple rows with filters
72
+ const apps = await selectAppsRowsWithFilters({ status: 'active' });
73
+
74
+ // Get a single row with filters
75
+ const app = await selectAppsSingleRowWithFilters({ id: 'your-app-id' });
76
+
77
+ // Get by ID
78
+ const appById = await selectAppsRowById('your-app-id');
79
+
80
+ // Create new row
81
+ const newApp = await insertAppsRow({ name: 'New App', status: 'active' });
82
+
83
+ // Update row
84
+ const updatedApp = await updateAppsRow({ id: 'your-app-id', name: 'Updated Name' });
85
+
86
+ // Delete row
87
+ const deletedApp = await deleteAppsRow('your-app-id');
88
+ ```
89
+
90
+ - All functions are async and return the corresponding row type.
91
+ - You can use filter objects for flexible queries.
92
+ - See the generated file for each table in `src/integrations/supabase/crud-autogen/` for details.
93
+
94
+ ## Limitations
95
+
96
+ Currently, the generated code only supports tables where the primary key column is named `id`.
97
+ If your table's primary key is not named `id`, the `selectById`, `update`, and `delete` functions will not be generated for that table.
98
+
99
+ ## Repository
100
+
101
+ For more details, see the project on GitHub: [https://github.com/idea-garage/supatool](https://github.com/idea-garage/supatool)
102
+
103
+ ## Acknowledgements
104
+
105
+ This project is inspired by and made possible thanks to the amazing work of [Supabase](https://supabase.com/).
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.helpText = void 0;
4
+ // See: [src/bin/helptext.ts](./src/bin/helptext.ts) from project root
5
+ // Help text (command section from README, English only)
6
+ exports.helpText = `
7
+ Supatool CLI - Generate TypeScript CRUD code from Supabase type definitions
8
+
9
+ Usage:
10
+ supatool crud [options]
11
+ supatool help
12
+
13
+ Commands:
14
+ crud Generate CRUD code
15
+ help Show help
16
+
17
+ Options:
18
+ -i, --import <path> Import path for type definitions (default: shared/)
19
+ -e, --export <path> Output path for CRUD code (default: src/integrations/supabase/)
20
+ -t, --tables <names> Generate code for specific tables only (comma separated, e.g. table1,table2)
21
+ -f, --force Overwrite output folder without confirmation
22
+ -h, --help Show help
23
+ -V, --version Show version
24
+
25
+ Examples:
26
+ supatool crud
27
+ - Import path: shared/
28
+ - Export path: src/integrations/supabase/
29
+
30
+ supatool crud -i path/to/import -e path/to/export
31
+ - Import path: path/to/import
32
+ - Export path: path/to/export
33
+
34
+ supatool crud -t users,posts
35
+ - Only generate for users and posts tables
36
+ `;
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ // CLI entry point
5
+ // Subcommand support with commander
6
+ const commander_1 = require("commander");
7
+ const index_1 = require("../index");
8
+ const helptext_js_1 = require("./helptext.js"); // Import help text from external file
9
+ const program = new commander_1.Command();
10
+ program
11
+ .name('supatool')
12
+ .description('Supatool CLI')
13
+ .version('0.1.0');
14
+ // crud subcommand
15
+ program
16
+ .command('crud')
17
+ .description('Generate CRUD types')
18
+ .option('-i, --import <path>', 'Import path for type definitions', 'shared/')
19
+ .option('-e, --export <path>', 'Output path for CRUD code', 'src/integrations/supabase/')
20
+ .option('-t, --tables <tables>', 'Generate only for specific tables (comma separated)')
21
+ .option('-f, --force', 'Force overwrite output folder without confirmation')
22
+ .action((options) => {
23
+ // Reflect command line arguments to process.argv (for main() reuse)
24
+ const args = process.argv.slice(0, 2);
25
+ if (options.import) {
26
+ args.push('-i', options.import);
27
+ }
28
+ if (options.export) {
29
+ args.push('-e', options.export);
30
+ }
31
+ if (options.force) {
32
+ args.push('--force');
33
+ }
34
+ process.argv = args;
35
+ (0, index_1.main)();
36
+ });
37
+ // help subcommand
38
+ program
39
+ .command('help')
40
+ .description('Show help')
41
+ .action(() => {
42
+ console.log(helptext_js_1.helpText);
43
+ });
44
+ // If no subcommand is specified, show helpText only (do not call main)
45
+ if (!process.argv.slice(2).length) {
46
+ console.log(helptext_js_1.helpText);
47
+ process.exit(0);
48
+ }
49
+ program.parse(process.argv);
package/dist/index.js ADDED
@@ -0,0 +1,314 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-var-requires */
3
+ // Generate TypeScript CRUD code from Supabase type definitions
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.main = main;
9
+ const typescript_1 = require("typescript");
10
+ const fs_1 = require("fs");
11
+ const path_1 = __importDefault(require("path"));
12
+ // Get paths from command line arguments (with default values)
13
+ const args = process.argv.slice(2);
14
+ let importPath = 'shared/';
15
+ let exportPath = 'src/integrations/supabase/';
16
+ let tables = null;
17
+ for (let i = 0; i < args.length; i++) {
18
+ if (args[i] === '-i' && args[i + 1]) {
19
+ importPath = args[i + 1];
20
+ i++;
21
+ }
22
+ else if (args[i] === '-e' && args[i + 1]) {
23
+ exportPath = args[i + 1];
24
+ i++;
25
+ }
26
+ else if ((args[i] === '--tables' || args[i] === '-t') && args[i + 1]) {
27
+ tables = args[i + 1].split(',').map((t) => t.trim());
28
+ i++;
29
+ }
30
+ }
31
+ // Convert to absolute paths
32
+ const resolvedImportPath = path_1.default.resolve(process.cwd(), importPath);
33
+ const resolvedExportPath = path_1.default.resolve(process.cwd(), exportPath);
34
+ const typeDefinitionsPath = path_1.default.join(resolvedImportPath, 'types.ts');
35
+ const crudFolderPath = path_1.default.join(resolvedExportPath, 'crud-autogen/');
36
+ // Main process as a function
37
+ function main() {
38
+ // Check --force option
39
+ const force = process.argv.includes('--force') || process.argv.includes('-f');
40
+ // Check if type definition file exists
41
+ if (!(0, fs_1.existsSync)(typeDefinitionsPath)) {
42
+ console.error(`Error: Type definition file not found: ${typeDefinitionsPath}`);
43
+ console.error('Please generate it with:');
44
+ console.error(' npx supabase gen types typescript --project-id "your-project-id" --schema public > shared/types.ts');
45
+ process.exit(1);
46
+ }
47
+ // Check if output folder exists
48
+ if ((0, fs_1.existsSync)(crudFolderPath) && !force) {
49
+ // Confirm via standard input
50
+ const readline = require('readline');
51
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
52
+ rl.question(`Output folder already exists: ${crudFolderPath}\nOverwrite? (y/N): `, (answer) => {
53
+ rl.close();
54
+ if (answer.toLowerCase() !== 'y') {
55
+ console.log('Canceled.');
56
+ process.exit(0);
57
+ }
58
+ else {
59
+ generateCrud();
60
+ }
61
+ });
62
+ return;
63
+ }
64
+ generateCrud();
65
+ function generateCrud() {
66
+ // Read type definition file
67
+ const typeDefinitions = (0, fs_1.readFileSync)(typeDefinitionsPath, 'utf-8');
68
+ // Parse types using TypeScript parser
69
+ const sourceFile = (0, typescript_1.createSourceFile)(typeDefinitionsPath, typeDefinitions, typescript_1.ScriptTarget.Latest);
70
+ // Create program and get type checker
71
+ const program = (0, typescript_1.createProgram)([typeDefinitionsPath], {});
72
+ const typeChecker = program.getTypeChecker();
73
+ // Function to get schema name dynamically
74
+ const getSchemaName = (typeNode) => {
75
+ return typeNode.members.find((member) => member.name && member.name.text)
76
+ ?.name.text;
77
+ };
78
+ // Extract types (only Database type)
79
+ const types = sourceFile.statements
80
+ .filter(stmt => stmt.kind === typescript_1.SyntaxKind.TypeAliasDeclaration)
81
+ .flatMap(typeAliasDecl => {
82
+ const typeName = typeAliasDecl.name.text;
83
+ const typeNode = typeAliasDecl.type;
84
+ if (typeNode.kind === typescript_1.SyntaxKind.TypeLiteral && typeName === 'Database') {
85
+ const schemaName = getSchemaName(typeNode);
86
+ const schemaType = typeNode.members.find((member) => member.name && member.name.text === schemaName);
87
+ if (schemaType && schemaType.type.kind === typescript_1.SyntaxKind.TypeLiteral) {
88
+ const tablesAndViewsType = schemaType.type.members.filter((member) => member.name && (member.name.text === 'Tables' || member.name.text === 'Views'));
89
+ return tablesAndViewsType.flatMap((tablesOrViewsType) => {
90
+ if (tablesOrViewsType.type.kind === typescript_1.SyntaxKind.TypeLiteral) {
91
+ return tablesOrViewsType.type.members.map((tableOrViewMember) => {
92
+ const tableName = tableOrViewMember.name.text;
93
+ const isView = tablesOrViewsType.name.text === 'Views';
94
+ const rowType = tableOrViewMember.type.members.find((member) => member.name && member.name.text === 'Row');
95
+ if (rowType && rowType.type.kind === typescript_1.SyntaxKind.TypeLiteral) {
96
+ const fields = rowType.type.members.map((member) => {
97
+ if (member.name && member.name.kind === typescript_1.SyntaxKind.Identifier) {
98
+ const name = member.name.getText(sourceFile);
99
+ const type = member.type ? member.type.getText(sourceFile) : 'unknown';
100
+ return { name, type };
101
+ }
102
+ return { name: 'unknown', type: 'unknown' };
103
+ });
104
+ return { typeName: tableName, fields, isView };
105
+ }
106
+ return null;
107
+ }).filter((type) => type !== null);
108
+ }
109
+ return [];
110
+ });
111
+ }
112
+ }
113
+ return [];
114
+ });
115
+ // Show start of generation process
116
+ console.log(`Import path: ${importPath}`);
117
+ console.log(`Export path: ${exportPath}`);
118
+ // Create CRUD folder if it does not exist
119
+ if (!(0, fs_1.existsSync)(crudFolderPath)) {
120
+ (0, fs_1.mkdirSync)(crudFolderPath, { recursive: true });
121
+ }
122
+ // Generate CRUD code for each type
123
+ types
124
+ .filter(type => {
125
+ // If tables are specified, generate only for those table names
126
+ if (tables && tables.length > 0) {
127
+ return tables.includes(type.typeName);
128
+ }
129
+ return true;
130
+ })
131
+ .forEach(type => {
132
+ const fileName = toLowerCamelCase(type.typeName);
133
+ const crudCode = crudTemplate(type.typeName, type.fields, type.isView);
134
+ const filePath = crudFolderPath + `${fileName}.ts`;
135
+ // Show in console
136
+ if (type.isView) {
137
+ console.log(`Generating select operations only for view: ${fileName}`);
138
+ }
139
+ else {
140
+ console.log(`Generating full CRUD operations for table: ${fileName}`);
141
+ }
142
+ // Create directory if it does not exist
143
+ const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
144
+ if (!(0, fs_1.existsSync)(dirPath)) {
145
+ (0, fs_1.mkdirSync)(dirPath, { recursive: true });
146
+ }
147
+ (0, fs_1.writeFileSync)(filePath, crudCode);
148
+ console.log(`Generated ${fileName}.ts`);
149
+ });
150
+ console.log("CRUD operations have been generated.");
151
+ }
152
+ }
153
+ // Function to convert a string to lower camel case
154
+ const toLowerCamelCase = (str) => {
155
+ return str.split('_').map((word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join('');
156
+ };
157
+ // Function to convert a string to upper camel case
158
+ const toUpperCamelCase = (str) => {
159
+ return str.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('');
160
+ };
161
+ // CRUD template
162
+ const crudTemplate = (typeName, fields, isView) => {
163
+ const upperCamelTypeName = toUpperCamelCase(typeName);
164
+ const selectByIdFunctionName = 'select' + upperCamelTypeName + 'RowById';
165
+ const selectFilteredRowsFunctionName = 'select' + upperCamelTypeName + 'RowsWithFilters';
166
+ const selectFilteredSingleRowFunctionName = 'select' + upperCamelTypeName + 'SingleRowWithFilters';
167
+ const insertFunctionName = 'insert' + upperCamelTypeName + 'Row';
168
+ const updateFunctionName = 'update' + upperCamelTypeName + 'Row';
169
+ const deleteFunctionName = 'delete' + upperCamelTypeName + 'Row';
170
+ const idType = fields.find((field) => field.name === 'id')?.type;
171
+ const hasIdColumn = idType !== undefined; // Check if 'id' column exists
172
+ const exportHeaders = `// Supabase CRUD operations for ${typeName}
173
+ // This file is automatically generated. Do not edit it directly.
174
+ import { supabase } from "../client";
175
+ import { Tables, TablesInsert, TablesUpdate } from "@shared/types";
176
+
177
+ type ${typeName} = Tables<'${typeName}'>;
178
+ type FilterTypesValue = string | number | boolean | null | Record<string, any>;
179
+ type Filters = Record<string, FilterTypesValue | FilterTypesValue[]>;
180
+ `;
181
+ const exportSelectById = `
182
+ // Read single row using id
183
+ async function ${selectByIdFunctionName}(id: ${idType}): Promise<${typeName} | ${typeName}[]> {
184
+ if (id !== undefined) {
185
+ const result = await supabase
186
+ .from('${typeName.toLowerCase()}')
187
+ .select('*')
188
+ .eq('id', id)
189
+ .single();
190
+ return result.data as ${typeName};
191
+ }
192
+ const result = await supabase.from('${typeName.toLowerCase()}').select('*');
193
+ return result.data as ${typeName}[];
194
+ }
195
+ `;
196
+ const exportSelectQueries = `
197
+ // Function to apply filters to a query
198
+ function applyFilters(query: any, filters: Filters): any {
199
+ for (const [key, value] of Object.entries(filters)) {
200
+ if (Array.isArray(value)) {
201
+ query = query.in(key, value); // Use 'in' for array values
202
+ } else if (typeof value === 'object' && value !== null) {
203
+ for (const [operator, val] of Object.entries(value)) {
204
+ switch (operator) {
205
+ case 'eq':
206
+ query = query.eq(key, val);
207
+ break;
208
+ case 'neq':
209
+ query = query.neq(key, val);
210
+ break;
211
+ case 'like':
212
+ query = query.like(key, val);
213
+ break;
214
+ case 'ilike':
215
+ query = query.ilike(key, val);
216
+ break;
217
+ case 'lt':
218
+ query = query.lt(key, val);
219
+ break;
220
+ case 'lte':
221
+ query = query.lte(key, val);
222
+ break;
223
+ case 'gte':
224
+ query = query.gte(key, val);
225
+ break;
226
+ case 'gt':
227
+ query = query.gt(key, val);
228
+ break;
229
+ case 'contains':
230
+ query = query.contains(key, val);
231
+ break;
232
+ case 'contains_any':
233
+ query = query.contains_any(key, val);
234
+ break;
235
+ case 'contains_all':
236
+ query = query.contains_all(key, val);
237
+ break;
238
+ // Add more operators as needed
239
+ default:
240
+ throw new Error('Unsupported operator: ' + operator);
241
+ }
242
+ }
243
+ } else {
244
+ query = query.eq(key, value); // Default to 'eq' for simple values
245
+ }
246
+ }
247
+ return query;
248
+ }
249
+
250
+ // Read multiple rows with dynamic filters
251
+ async function ${selectFilteredRowsFunctionName}(filters: Filters = {}): Promise<${typeName}[]> {
252
+ let query = supabase.from('${typeName.toLowerCase()}').select('*');
253
+ query = applyFilters(query, filters);
254
+
255
+ const result = await query;
256
+ return result.data as ${typeName}[];
257
+ }
258
+
259
+ // Read a single row with dynamic filters
260
+ async function ${selectFilteredSingleRowFunctionName}(filters: Filters = {}): Promise<${typeName}> {
261
+ let query = supabase.from('${typeName.toLowerCase()}').select('*');
262
+ query = applyFilters(query, filters).single();
263
+
264
+ const result = await query;
265
+ return result.data as unknown as ${typeName};
266
+ }
267
+ `;
268
+ const exportInsertOperation = isView ? '' :
269
+ `
270
+ // Create Function
271
+ async function ${insertFunctionName}(data: TablesInsert<'${typeName}'>): Promise<${typeName}> {
272
+ const result = await supabase
273
+ .from('${typeName}')
274
+ .insert([data])
275
+ .select()
276
+ .single();
277
+
278
+ if (result.data) {
279
+ return result.data as ${typeName};
280
+ }
281
+ throw new Error('Failed to insert data');
282
+ }
283
+ `;
284
+ const exportUpdateOperation = isView ? '' :
285
+ `
286
+ // Update Function
287
+ async function ${updateFunctionName}(data: TablesUpdate<'${typeName}'> & { id: ${idType} }): Promise<${typeName}> {
288
+ const result = await supabase.from('${typeName.toLowerCase()}').update(data).eq('id', data.id).select().single();
289
+ if (result.data) {
290
+ return result.data as ${typeName};
291
+ }
292
+ throw new Error('Failed to update data');
293
+ }
294
+ `;
295
+ const exportDeleteOperation = isView ? '' :
296
+ `
297
+ // Delete Function
298
+ async function ${deleteFunctionName}(id: ${idType}): Promise<${typeName}> {
299
+ const result = await supabase.from('${typeName.toLowerCase()}').delete().eq('id', id).select().single();
300
+ if (result.data) {
301
+ return result.data as ${typeName};
302
+ }
303
+ throw new Error('Failed to delete data');
304
+ }
305
+ `;
306
+ // Export all functions
307
+ const exportAll = `
308
+ export { ${selectFilteredRowsFunctionName}, ${selectFilteredSingleRowFunctionName}${hasIdColumn ? ', ' + selectByIdFunctionName : ''}${isView ? '' : ', ' + insertFunctionName + ', ' + updateFunctionName + ', ' + deleteFunctionName} };
309
+ `;
310
+ // Return all the code
311
+ return exportHeaders + exportSelectQueries + (hasIdColumn ? exportSelectById : '') + exportInsertOperation + exportUpdateOperation + exportDeleteOperation + exportAll;
312
+ };
313
+ // console.log(crudFolderPath);
314
+ // console.log(types);
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "supatool",
3
+ "version": "0.1.0",
4
+ "description": "A CLI tool that automatically generates TypeScript CRUD code from Supabase type definitions.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "supatool": "dist/bin/supatool.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "tsx bin/supatool.ts"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin"
17
+ ],
18
+ "keywords": [
19
+ "supabase",
20
+ "crud",
21
+ "cli",
22
+ "typescript",
23
+ "React",
24
+ "postgres",
25
+ "database"
26
+ ],
27
+ "author": "IdeaGarage",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "tsx": "^4.7.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.17.30",
35
+ "commander": "^13.1.0"
36
+ }
37
+ }