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 +21 -0
- package/README.md +105 -0
- package/dist/bin/helptext.js +36 -0
- package/dist/bin/supatool.js +49 -0
- package/dist/index.js +314 -0
- package/package.json +37 -0
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
|
+
}
|