supatool 0.3.5 → 0.3.7
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/generator/sqlGenerator.js +38 -10
- package/dist/index.js +249 -270
- package/dist/sync/definitionExtractor.js +17 -0
- package/dist/sync/fetchRemoteSchemas.js +16 -145
- package/package.json +1 -1
@@ -65,26 +65,54 @@ function generateSqlFromModel(model, outPath) {
|
|
65
65
|
}
|
66
66
|
}
|
67
67
|
let def = ` ${actualColName} ${toSqlType(c.type, actualColName)}`;
|
68
|
-
if (c.primary)
|
69
|
-
def += ' PRIMARY KEY';
|
70
|
-
if (c.unique)
|
71
|
-
def += ' UNIQUE';
|
72
68
|
if (c.notNull)
|
73
69
|
def += ' NOT NULL';
|
74
70
|
if (c.default)
|
75
71
|
def += ` DEFAULT ${c.default}`;
|
76
|
-
// 外部キー制約はテーブル末尾に付与するため保留
|
77
72
|
colDefs.push(def);
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
73
|
+
}
|
74
|
+
// 主キー制約(constraint名付き)
|
75
|
+
const pkCols = Object.entries(t.fields || {})
|
76
|
+
.filter(([_, col]) => col.primary)
|
77
|
+
.map(([colName, _]) => colName);
|
78
|
+
if (pkCols.length > 0) {
|
79
|
+
tableConstraints.push(` CONSTRAINT ${tableName}_pkey PRIMARY KEY (${pkCols.join(', ')})`);
|
80
|
+
}
|
81
|
+
// ユニーク制約(constraint名付き)
|
82
|
+
if (Array.isArray(t.uniques)) {
|
83
|
+
for (const unique of t.uniques) {
|
84
|
+
if (Array.isArray(unique.columns) && unique.name) {
|
85
|
+
tableConstraints.push(` CONSTRAINT ${unique.name} UNIQUE (${unique.columns.join(', ')})`);
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
// 外部キー制約(constraint名付き)
|
90
|
+
if (Array.isArray(t.foreignKeys)) {
|
91
|
+
for (const fk of t.foreignKeys) {
|
92
|
+
if (fk.name && fk.columns && fk.refTable && fk.refColumns) {
|
93
|
+
let fkStr = ` CONSTRAINT ${fk.name} FOREIGN KEY (${fk.columns.join(', ')}) REFERENCES ${fk.refTable} (${fk.refColumns.join(', ')})`;
|
94
|
+
if (fk.onDelete)
|
95
|
+
fkStr += ` ON DELETE ${fk.onDelete}`;
|
96
|
+
if (fk.onUpdate)
|
97
|
+
fkStr += ` ON UPDATE ${fk.onUpdate}`;
|
98
|
+
tableConstraints.push(fkStr);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
// チェック制約(constraint名付き)
|
103
|
+
if (Array.isArray(t.checkConstraints)) {
|
104
|
+
for (const check of t.checkConstraints) {
|
105
|
+
if (check.name) {
|
106
|
+
tableConstraints.push(` CONSTRAINT ${check.name} CHECK (${check.expression})`);
|
107
|
+
}
|
108
|
+
else {
|
109
|
+
tableConstraints.push(` CHECK (${check.expression})`);
|
110
|
+
}
|
82
111
|
}
|
83
112
|
}
|
84
113
|
// 列定義 + テーブルレベル制約を結合
|
85
114
|
const defs = [...colDefs, ...tableConstraints];
|
86
115
|
sql += defs.join(',\n') + '\n);\n\n';
|
87
|
-
// ALTER TABLEによる外部キー制約は出力しない
|
88
116
|
sql += '\n';
|
89
117
|
}
|
90
118
|
fs_1.default.writeFileSync(outPath, sql);
|
package/dist/index.js
CHANGED
@@ -82,36 +82,50 @@ function main() {
|
|
82
82
|
const typeName = typeAliasDecl.name.text;
|
83
83
|
const typeNode = typeAliasDecl.type;
|
84
84
|
if (typeNode.kind === typescript_1.SyntaxKind.TypeLiteral && typeName === 'Database') {
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
85
|
+
console.log('Found Database type, processing schemas...');
|
86
|
+
// Database型内の全てのスキーマを処理
|
87
|
+
return typeNode.members.flatMap((schemaMember) => {
|
88
|
+
if (schemaMember.name && schemaMember.type && schemaMember.type.kind === typescript_1.SyntaxKind.TypeLiteral) {
|
89
|
+
const schemaName = schemaMember.name.text;
|
90
|
+
console.log(`Processing schema: ${schemaName}`);
|
91
|
+
const schemaType = schemaMember.type;
|
92
|
+
// スキーマ内のTablesとViewsを処理
|
93
|
+
const tablesAndViewsType = schemaType.members.filter((member) => member.name && (member.name.text === 'Tables' || member.name.text === 'Views'));
|
94
|
+
return tablesAndViewsType.flatMap((tablesOrViewsType) => {
|
95
|
+
if (tablesOrViewsType.type.kind === typescript_1.SyntaxKind.TypeLiteral) {
|
96
|
+
const tableCount = tablesOrViewsType.type.members.length;
|
97
|
+
console.log(`Found ${tableCount} ${tablesOrViewsType.name.text} in schema ${schemaName}`);
|
98
|
+
return tablesOrViewsType.type.members.map((tableOrViewMember) => {
|
99
|
+
const tableName = tableOrViewMember.name.text;
|
100
|
+
const isView = tablesOrViewsType.name.text === 'Views';
|
101
|
+
console.log(`Processing ${isView ? 'view' : 'table'}: ${tableName}`);
|
102
|
+
const rowType = tableOrViewMember.type.members.find((member) => member.name && member.name.text === 'Row');
|
103
|
+
if (rowType && rowType.type.kind === typescript_1.SyntaxKind.TypeLiteral) {
|
104
|
+
const fields = rowType.type.members.map((member) => {
|
105
|
+
if (member.name && member.name.kind === typescript_1.SyntaxKind.Identifier) {
|
106
|
+
const name = member.name.getText(sourceFile);
|
107
|
+
const type = member.type ? member.type.getText(sourceFile) : 'unknown';
|
108
|
+
return { name, type };
|
109
|
+
}
|
110
|
+
return { name: 'unknown', type: 'unknown' };
|
111
|
+
});
|
112
|
+
return { typeName: tableName, fields, isView, schema: schemaName };
|
113
|
+
}
|
114
|
+
return null;
|
115
|
+
}).filter((type) => type !== null);
|
116
|
+
}
|
117
|
+
return [];
|
118
|
+
});
|
119
|
+
}
|
120
|
+
return [];
|
121
|
+
});
|
112
122
|
}
|
113
123
|
return [];
|
114
124
|
});
|
125
|
+
console.log(`Total types found: ${types.length}`);
|
126
|
+
types.forEach(type => {
|
127
|
+
console.log(`- ${type.schema}.${type.typeName} (${type.isView ? 'view' : 'table'}) with ${type.fields.length} fields`);
|
128
|
+
});
|
115
129
|
// Show start of generation process
|
116
130
|
console.log(`Import path: ${importPath}`);
|
117
131
|
console.log(`Export path: ${exportPath}`);
|
@@ -129,23 +143,25 @@ function main() {
|
|
129
143
|
return true;
|
130
144
|
})
|
131
145
|
.forEach(type => {
|
146
|
+
// スキーマごとにフォルダ分け
|
147
|
+
const schemaFolder = path_1.default.join(crudFolderPath, type.schema);
|
148
|
+
if (!(0, fs_1.existsSync)(schemaFolder)) {
|
149
|
+
(0, fs_1.mkdirSync)(schemaFolder, { recursive: true });
|
150
|
+
}
|
132
151
|
const fileName = toLowerCamelCase(type.typeName);
|
133
|
-
|
134
|
-
const
|
152
|
+
// スキーマフォルダ分けがある場合のインポートパス調整
|
153
|
+
const hasSchemaFolders = types.some(t => t.schema !== type.schema);
|
154
|
+
const crudCode = crudTemplate(type.typeName, type.fields, type.isView, type.schema, hasSchemaFolders);
|
155
|
+
const filePath = path_1.default.join(schemaFolder, `${fileName}.ts`);
|
135
156
|
// Show in console
|
136
157
|
if (type.isView) {
|
137
|
-
console.log(`Generating select operations only for view: ${fileName}`);
|
158
|
+
console.log(`Generating select operations only for view: ${type.schema}/${fileName}`);
|
138
159
|
}
|
139
160
|
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 });
|
161
|
+
console.log(`Generating full CRUD operations for table: ${type.schema}/${fileName}`);
|
146
162
|
}
|
147
163
|
(0, fs_1.writeFileSync)(filePath, crudCode);
|
148
|
-
console.log(`Generated ${fileName}.ts`);
|
164
|
+
console.log(`Generated ${type.schema}/${fileName}.ts`);
|
149
165
|
});
|
150
166
|
console.log("CRUD operations have been generated.");
|
151
167
|
}
|
@@ -158,8 +174,8 @@ const toLowerCamelCase = (str) => {
|
|
158
174
|
const toUpperCamelCase = (str) => {
|
159
175
|
return str.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('');
|
160
176
|
};
|
161
|
-
// CRUD
|
162
|
-
const crudTemplate = (typeName, fields, isView) => {
|
177
|
+
// CRUDテンプレート本体 - エレガントな文字列生成
|
178
|
+
const crudTemplate = (typeName, fields, isView, schema, hasSchemaFolders) => {
|
163
179
|
const upperCamelTypeName = toUpperCamelCase(typeName);
|
164
180
|
const getByIdFunctionName = 'select' + upperCamelTypeName + 'RowById';
|
165
181
|
const getByFiltersFunctionName = 'select' + upperCamelTypeName + 'RowsWithFilters';
|
@@ -168,235 +184,198 @@ const crudTemplate = (typeName, fields, isView) => {
|
|
168
184
|
const updateFunctionName = 'update' + upperCamelTypeName + 'Row';
|
169
185
|
const deleteFunctionName = 'delete' + upperCamelTypeName + 'Row';
|
170
186
|
const idType = fields.find((field) => field.name === 'id')?.type || 'string';
|
171
|
-
|
172
|
-
const
|
173
|
-
//
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
if (
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
}
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
}
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
if (
|
272
|
-
|
273
|
-
}
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
throw error
|
279
|
-
}
|
280
|
-
}
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
}
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
`
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
if (
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
throw error
|
331
|
-
}
|
332
|
-
}
|
333
|
-
|
334
|
-
|
335
|
-
`
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
}
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
console.error('Error in ${updateFunctionName}:', error);
|
366
|
-
throw error;
|
367
|
-
}
|
368
|
-
}
|
369
|
-
`;
|
370
|
-
const exportDeleteOperation = isView ? '' :
|
371
|
-
`
|
372
|
-
// Delete Function
|
373
|
-
export async function ${deleteFunctionName}({ id }: { id: ${idType} }): Promise<boolean> {
|
374
|
-
if (!id) {
|
375
|
-
throw new Error('ID is required for deletion');
|
376
|
-
}
|
377
|
-
try {
|
378
|
-
const result = await supabase
|
379
|
-
.from('${typeName.toLowerCase()}')
|
380
|
-
.delete()
|
381
|
-
.eq('id', id);
|
382
|
-
|
383
|
-
if (result.error) {
|
384
|
-
throw new Error(\`Failed to delete ${typeName}: \${result.error.message}\`);
|
385
|
-
}
|
386
|
-
|
387
|
-
return true;
|
388
|
-
} catch (error) {
|
389
|
-
console.error('Error in ${deleteFunctionName}:', error);
|
390
|
-
throw error;
|
391
|
-
}
|
392
|
-
}
|
393
|
-
`;
|
394
|
-
// Export all functions
|
395
|
-
const exportAll = `
|
396
|
-
// All functions are exported individually above
|
397
|
-
`;
|
398
|
-
// Return all the code
|
399
|
-
return exportHeaders + exportSelectQueries + (hasIdColumn ? exportSelectById : '') + exportInsertOperation + exportUpdateOperation + exportDeleteOperation + exportAll;
|
187
|
+
// インポートパスを動的に調整
|
188
|
+
const importPath = hasSchemaFolders ? '../../client' : '../client';
|
189
|
+
// ヘッダー部分
|
190
|
+
const header = [
|
191
|
+
`// Supabase CRUD operations for ${typeName} (${schema} schema)`,
|
192
|
+
'// 自動生成ファイル',
|
193
|
+
`import { supabase } from "${importPath}";`,
|
194
|
+
'import { Tables, TablesInsert, TablesUpdate } from "@shared/types";',
|
195
|
+
'',
|
196
|
+
`type ${typeName} = Tables<'${typeName}'>;`,
|
197
|
+
'type FilterTypesValue = string | number | boolean | null | Record<string, any>;',
|
198
|
+
'type Filters = Record<string, FilterTypesValue | FilterTypesValue[]>;',
|
199
|
+
''
|
200
|
+
].join('\n');
|
201
|
+
// フィルター適用関数
|
202
|
+
const filterFunction = [
|
203
|
+
'/**',
|
204
|
+
' * フィルター適用関数',
|
205
|
+
' */',
|
206
|
+
'function applyFilters(query: any, filters: Filters): any {',
|
207
|
+
' for (const [key, value] of Object.entries(filters)) {',
|
208
|
+
' if (Array.isArray(value)) {',
|
209
|
+
' query = query.in(key, value);',
|
210
|
+
' } else if (typeof value === "object" && value !== null) {',
|
211
|
+
' for (const [operator, val] of Object.entries(value)) {',
|
212
|
+
' switch (operator) {',
|
213
|
+
' case "eq": query = query.eq(key, val); break;',
|
214
|
+
' case "neq": query = query.neq(key, val); break;',
|
215
|
+
' case "like": query = query.like(key, val); break;',
|
216
|
+
' case "ilike": query = query.ilike(key, val); break;',
|
217
|
+
' case "lt": query = query.lt(key, val); break;',
|
218
|
+
' case "lte": query = query.lte(key, val); break;',
|
219
|
+
' case "gte": query = query.gte(key, val); break;',
|
220
|
+
' case "gt": query = query.gt(key, val); break;',
|
221
|
+
' case "contains": query = query.contains(key, val); break;',
|
222
|
+
' case "contains_any": query = query.contains_any(key, val); break;',
|
223
|
+
' case "contains_all": query = query.contains_all(key, val); break;',
|
224
|
+
' default: throw new Error("Unsupported operator: " + operator);',
|
225
|
+
' }',
|
226
|
+
' }',
|
227
|
+
' } else {',
|
228
|
+
' query = query.eq(key, value);',
|
229
|
+
' }',
|
230
|
+
' }',
|
231
|
+
' return query;',
|
232
|
+
'}',
|
233
|
+
''
|
234
|
+
].join('\n');
|
235
|
+
// IDで1件取得
|
236
|
+
const selectById = [
|
237
|
+
'/**',
|
238
|
+
' * IDで1件取得',
|
239
|
+
' */',
|
240
|
+
`export async function ${getByIdFunctionName}({ id }: { id: ${idType} }): Promise<${typeName} | null> {`,
|
241
|
+
' if (!id) throw new Error("ID is required");',
|
242
|
+
' try {',
|
243
|
+
' const result = await supabase',
|
244
|
+
` .schema("${schema}")`,
|
245
|
+
` .from("${typeName.toLowerCase()}")`,
|
246
|
+
' .select("*")',
|
247
|
+
' .eq("id", id)',
|
248
|
+
' .single();',
|
249
|
+
' if (result.error) {',
|
250
|
+
' if (result.error.code === "PGRST116") return null;',
|
251
|
+
` throw new Error(\`Failed to fetch ${typeName}: \${result.error.message}\`);`,
|
252
|
+
' }',
|
253
|
+
` return result.data as ${typeName};`,
|
254
|
+
' } catch (error) {',
|
255
|
+
` console.error("Error in ${getByIdFunctionName}:", error);`,
|
256
|
+
' throw error;',
|
257
|
+
' }',
|
258
|
+
'}',
|
259
|
+
''
|
260
|
+
].join('\n');
|
261
|
+
// フィルターで複数取得
|
262
|
+
const selectMultiple = [
|
263
|
+
'/**',
|
264
|
+
' * フィルターで複数取得',
|
265
|
+
' */',
|
266
|
+
`export async function ${getByFiltersFunctionName}({ filters }: { filters: Filters }): Promise<${typeName}[]> {`,
|
267
|
+
' if (!filters || typeof filters !== "object") return [];',
|
268
|
+
' try {',
|
269
|
+
` let query = supabase.schema("${schema}").from("${typeName.toLowerCase()}").select("*");`,
|
270
|
+
' query = applyFilters(query, filters);',
|
271
|
+
' const result = await query;',
|
272
|
+
` if (result.error) throw new Error(\`Failed to fetch ${typeName}: \${result.error.message}\`);`,
|
273
|
+
` return (result.data as unknown as ${typeName}[]) || [];`,
|
274
|
+
' } catch (error) {',
|
275
|
+
` console.error("Error in ${getByFiltersFunctionName}:", error);`,
|
276
|
+
' throw error;',
|
277
|
+
' }',
|
278
|
+
'}',
|
279
|
+
''
|
280
|
+
].join('\n');
|
281
|
+
// フィルターで1件取得
|
282
|
+
const selectSingle = [
|
283
|
+
'/**',
|
284
|
+
' * フィルターで1件取得',
|
285
|
+
' */',
|
286
|
+
`export async function ${getSingleByFiltersFunctionName}({ filters }: { filters: Filters }): Promise<${typeName} | null> {`,
|
287
|
+
' if (!filters || typeof filters !== "object") return null;',
|
288
|
+
' try {',
|
289
|
+
` let query = supabase.schema("${schema}").from("${typeName.toLowerCase()}").select("*");`,
|
290
|
+
' query = applyFilters(query, filters).single();',
|
291
|
+
' const result = await query;',
|
292
|
+
' if (result.error) {',
|
293
|
+
' if (result.error.code === "PGRST116") return null;',
|
294
|
+
` throw new Error(\`Failed to fetch ${typeName}: \${result.error.message}\`);`,
|
295
|
+
' }',
|
296
|
+
` return result.data as unknown as ${typeName};`,
|
297
|
+
' } catch (error) {',
|
298
|
+
` console.error("Error in ${getSingleByFiltersFunctionName}:", error);`,
|
299
|
+
' throw error;',
|
300
|
+
' }',
|
301
|
+
'}',
|
302
|
+
''
|
303
|
+
].join('\n');
|
304
|
+
// 追加(ビューでない場合のみ)
|
305
|
+
const insertOperation = isView ? '' : [
|
306
|
+
'/**',
|
307
|
+
' * 追加',
|
308
|
+
' */',
|
309
|
+
`export async function ${createFunctionName}({ data }: { data: TablesInsert<"${typeName}"> }): Promise<${typeName}> {`,
|
310
|
+
' if (!data) throw new Error("Data is required for creation");',
|
311
|
+
' try {',
|
312
|
+
' const result = await supabase',
|
313
|
+
` .schema("${schema}")`,
|
314
|
+
` .from("${typeName.toLowerCase()}")`,
|
315
|
+
' .insert([data])',
|
316
|
+
' .select()',
|
317
|
+
' .single();',
|
318
|
+
` if (result.error) throw new Error(\`Failed to create ${typeName}: \${result.error.message}\`);`,
|
319
|
+
' if (!result.data) throw new Error("No data returned after creation");',
|
320
|
+
` return result.data as ${typeName};`,
|
321
|
+
' } catch (error) {',
|
322
|
+
` console.error("Error in ${createFunctionName}:", error);`,
|
323
|
+
' throw error;',
|
324
|
+
' }',
|
325
|
+
'}',
|
326
|
+
''
|
327
|
+
].join('\n');
|
328
|
+
// 更新(ビューでない場合のみ)
|
329
|
+
const updateOperation = isView ? '' : [
|
330
|
+
'/**',
|
331
|
+
' * 更新',
|
332
|
+
' */',
|
333
|
+
`export async function ${updateFunctionName}({ id, data }: { id: ${idType}; data: TablesUpdate<"${typeName}"> }): Promise<${typeName}> {`,
|
334
|
+
' if (!id) throw new Error("ID is required for update");',
|
335
|
+
' if (!data || Object.keys(data).length === 0) throw new Error("Update data is required");',
|
336
|
+
' try {',
|
337
|
+
' const result = await supabase',
|
338
|
+
` .schema("${schema}")`,
|
339
|
+
` .from("${typeName.toLowerCase()}")`,
|
340
|
+
' .update(data)',
|
341
|
+
' .eq("id", id)',
|
342
|
+
' .select()',
|
343
|
+
' .single();',
|
344
|
+
' if (result.error) {',
|
345
|
+
` if (result.error.code === "PGRST116") throw new Error(\`${typeName} with ID \${id} not found\`);`,
|
346
|
+
` throw new Error(\`Failed to update ${typeName}: \${result.error.message}\`);`,
|
347
|
+
' }',
|
348
|
+
` if (!result.data) throw new Error(\`${typeName} with ID \${id} not found\`);`,
|
349
|
+
` return result.data as ${typeName};`,
|
350
|
+
' } catch (error) {',
|
351
|
+
` console.error("Error in ${updateFunctionName}:", error);`,
|
352
|
+
' throw error;',
|
353
|
+
' }',
|
354
|
+
'}',
|
355
|
+
''
|
356
|
+
].join('\n');
|
357
|
+
// 削除(ビューでない場合のみ)
|
358
|
+
const deleteOperation = isView ? '' : [
|
359
|
+
'/**',
|
360
|
+
' * 削除',
|
361
|
+
' */',
|
362
|
+
`export async function ${deleteFunctionName}({ id }: { id: ${idType} }): Promise<boolean> {`,
|
363
|
+
' if (!id) throw new Error("ID is required for deletion");',
|
364
|
+
' try {',
|
365
|
+
' const result = await supabase',
|
366
|
+
` .schema("${schema}")`,
|
367
|
+
` .from("${typeName.toLowerCase()}")`,
|
368
|
+
' .delete()',
|
369
|
+
' .eq("id", id);',
|
370
|
+
` if (result.error) throw new Error(\`Failed to delete ${typeName}: \${result.error.message}\`);`,
|
371
|
+
' return true;',
|
372
|
+
' } catch (error) {',
|
373
|
+
` console.error("Error in ${deleteFunctionName}:", error);`,
|
374
|
+
' throw error;',
|
375
|
+
' }',
|
376
|
+
'}',
|
377
|
+
''
|
378
|
+
].join('\n');
|
379
|
+
// 全体を結合
|
380
|
+
return header + filterFunction + selectById + selectMultiple + selectSingle + insertOperation + updateOperation + deleteOperation;
|
400
381
|
};
|
401
|
-
// console.log(crudFolderPath);
|
402
|
-
// console.log(types);
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
34
34
|
})();
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
36
36
|
exports.extractDefinitions = extractDefinitions;
|
37
|
+
exports.generateCreateTableDDL = generateCreateTableDDL;
|
37
38
|
const pg_1 = require("pg");
|
38
39
|
/**
|
39
40
|
* 進行状況を表示
|
@@ -869,6 +870,22 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
869
870
|
for (const fk of foreignKeyResult.rows) {
|
870
871
|
ddl += `,\n CONSTRAINT ${fk.constraint_name} FOREIGN KEY (${fk.columns}) REFERENCES ${fk.foreign_table_schema}.${fk.foreign_table_name} (${fk.foreign_columns})`;
|
871
872
|
}
|
873
|
+
// CHECK制約をCREATE TABLE内に追加(必ず最後)
|
874
|
+
const checkConstraintResult = await client.query(`
|
875
|
+
SELECT
|
876
|
+
con.conname as constraint_name,
|
877
|
+
pg_get_constraintdef(con.oid) as check_clause
|
878
|
+
FROM pg_constraint con
|
879
|
+
JOIN pg_class rel ON rel.oid = con.conrelid
|
880
|
+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
881
|
+
WHERE lower(rel.relname) = lower($1)
|
882
|
+
AND nsp.nspname = $2
|
883
|
+
AND con.contype = 'c'
|
884
|
+
ORDER BY con.conname
|
885
|
+
`, [tableName, schemaName]);
|
886
|
+
for (const check of checkConstraintResult.rows) {
|
887
|
+
ddl += `,\n CONSTRAINT ${check.constraint_name} ${check.check_clause}`;
|
888
|
+
}
|
872
889
|
ddl += '\n);\n\n';
|
873
890
|
// COMMENT ON文を追加
|
874
891
|
if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
|
@@ -37,6 +37,7 @@ exports.fetchRemoteSchemas = fetchRemoteSchemas;
|
|
37
37
|
const pg_1 = require("pg");
|
38
38
|
const dns = __importStar(require("dns"));
|
39
39
|
const util_1 = require("util");
|
40
|
+
const definitionExtractor_1 = require("./definitionExtractor");
|
40
41
|
const dnsLookup = (0, util_1.promisify)(dns.lookup);
|
41
42
|
/**
|
42
43
|
* DDL文字列を正規化(空白・改行・タブを統一)
|
@@ -163,162 +164,32 @@ async function fetchRemoteSchemas(connectionString) {
|
|
163
164
|
// 各テーブルのスキーマ情報を取得
|
164
165
|
for (const row of tablesResult.rows) {
|
165
166
|
const tableName = row.tablename;
|
166
|
-
//
|
167
|
-
let timestamp = Math.floor(Date.now() / 1000);
|
168
|
-
// pg_stat_user_tablesから取得
|
167
|
+
// テーブルの最終更新時刻を取得(省略: 既存ロジック流用)
|
168
|
+
let timestamp = Math.floor(Date.now() / 1000);
|
169
169
|
try {
|
170
170
|
const tableStatsResult = await client.query(`
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
171
|
+
SELECT
|
172
|
+
EXTRACT(EPOCH FROM GREATEST(
|
173
|
+
COALESCE(last_vacuum, '1970-01-01'::timestamp),
|
174
|
+
COALESCE(last_autovacuum, '1970-01-01'::timestamp),
|
175
|
+
COALESCE(last_analyze, '1970-01-01'::timestamp),
|
176
|
+
COALESCE(last_autoanalyze, '1970-01-01'::timestamp)
|
177
|
+
))::bigint as last_updated
|
178
|
+
FROM pg_stat_user_tables
|
179
|
+
WHERE relname = $1 AND schemaname = 'public'
|
180
|
+
`, [tableName]);
|
181
181
|
if (tableStatsResult.rows.length > 0 && tableStatsResult.rows[0].last_updated > 0) {
|
182
182
|
timestamp = tableStatsResult.rows[0].last_updated;
|
183
183
|
}
|
184
184
|
else {
|
185
|
-
// デフォルトタイムスタンプ(十分に古い固定値)
|
186
185
|
timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
187
186
|
}
|
188
187
|
}
|
189
|
-
catch
|
190
|
-
// デフォルトタイムスタンプ(十分に古い固定値)
|
188
|
+
catch {
|
191
189
|
timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
192
190
|
}
|
193
|
-
//
|
194
|
-
const
|
195
|
-
SELECT
|
196
|
-
column_name,
|
197
|
-
data_type,
|
198
|
-
character_maximum_length,
|
199
|
-
is_nullable,
|
200
|
-
column_default
|
201
|
-
FROM information_schema.columns
|
202
|
-
WHERE table_schema = 'public'
|
203
|
-
AND table_name = $1
|
204
|
-
ORDER BY ordinal_position
|
205
|
-
`, [tableName]);
|
206
|
-
// 主キー情報を取得
|
207
|
-
const primaryKeyResult = await client.query(`
|
208
|
-
SELECT column_name
|
209
|
-
FROM information_schema.table_constraints tc
|
210
|
-
JOIN information_schema.key_column_usage kcu
|
211
|
-
ON tc.constraint_name = kcu.constraint_name
|
212
|
-
WHERE tc.table_schema = 'public'
|
213
|
-
AND tc.table_name = $1
|
214
|
-
AND tc.constraint_type = 'PRIMARY KEY'
|
215
|
-
ORDER BY kcu.ordinal_position
|
216
|
-
`, [tableName]);
|
217
|
-
// CREATE TABLE文を生成
|
218
|
-
let ddl = `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
|
219
|
-
const columnDefs = [];
|
220
|
-
for (const col of columnsResult.rows) {
|
221
|
-
let colDef = ` ${col.column_name} ${col.data_type.toUpperCase()}`;
|
222
|
-
// 長さ指定
|
223
|
-
if (col.character_maximum_length) {
|
224
|
-
colDef += `(${col.character_maximum_length})`;
|
225
|
-
}
|
226
|
-
// NOT NULL制約
|
227
|
-
if (col.is_nullable === 'NO') {
|
228
|
-
colDef += ' NOT NULL';
|
229
|
-
}
|
230
|
-
// デフォルト値
|
231
|
-
if (col.column_default) {
|
232
|
-
colDef += ` DEFAULT ${col.column_default}`;
|
233
|
-
}
|
234
|
-
columnDefs.push(colDef);
|
235
|
-
}
|
236
|
-
ddl += columnDefs.join(',\n');
|
237
|
-
// 主キー制約
|
238
|
-
if (primaryKeyResult.rows.length > 0) {
|
239
|
-
const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
|
240
|
-
ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
|
241
|
-
}
|
242
|
-
// UNIQUE制約を取得してCREATE TABLE内に含める
|
243
|
-
const uniqueConstraintResult = await client.query(`
|
244
|
-
SELECT
|
245
|
-
tc.constraint_name,
|
246
|
-
string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as columns
|
247
|
-
FROM information_schema.table_constraints tc
|
248
|
-
JOIN information_schema.key_column_usage kcu
|
249
|
-
ON tc.constraint_name = kcu.constraint_name
|
250
|
-
WHERE tc.table_schema = 'public'
|
251
|
-
AND tc.table_name = $1
|
252
|
-
AND tc.constraint_type = 'UNIQUE'
|
253
|
-
GROUP BY tc.constraint_name
|
254
|
-
ORDER BY tc.constraint_name
|
255
|
-
`, [tableName]);
|
256
|
-
// UNIQUE制約をCREATE TABLE内に追加
|
257
|
-
for (const unique of uniqueConstraintResult.rows) {
|
258
|
-
ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
|
259
|
-
}
|
260
|
-
// CHECK制約は一時的に無効化(NOT NULL制約と重複するため)
|
261
|
-
// const checkConstraintResult = await client.query(`...`);
|
262
|
-
// CHECK制約をCREATE TABLE内に追加は一時的に無効化
|
263
|
-
// for (const check of checkConstraintResult.rows) {
|
264
|
-
// ddl += `,\n CONSTRAINT ${check.constraint_name} CHECK ${check.check_clause}`;
|
265
|
-
// }
|
266
|
-
ddl += '\n);';
|
267
|
-
// 外部キー制約のみを別途生成
|
268
|
-
const foreignKeyResult = await client.query(`
|
269
|
-
SELECT
|
270
|
-
tc.constraint_name,
|
271
|
-
kcu.column_name,
|
272
|
-
ccu.table_name AS foreign_table_name,
|
273
|
-
ccu.column_name AS foreign_column_name,
|
274
|
-
rc.delete_rule,
|
275
|
-
rc.update_rule
|
276
|
-
FROM information_schema.table_constraints tc
|
277
|
-
JOIN information_schema.key_column_usage kcu
|
278
|
-
ON tc.constraint_name = kcu.constraint_name
|
279
|
-
JOIN information_schema.constraint_column_usage ccu
|
280
|
-
ON ccu.constraint_name = tc.constraint_name
|
281
|
-
JOIN information_schema.referential_constraints rc
|
282
|
-
ON tc.constraint_name = rc.constraint_name
|
283
|
-
WHERE tc.table_schema = 'public'
|
284
|
-
AND tc.table_name = $1
|
285
|
-
AND tc.constraint_type = 'FOREIGN KEY'
|
286
|
-
ORDER BY tc.constraint_name
|
287
|
-
`, [tableName]);
|
288
|
-
// 外部キー制約のみをALTER TABLE文として追加
|
289
|
-
for (const fk of foreignKeyResult.rows) {
|
290
|
-
ddl += `\n\nALTER TABLE ${tableName} ADD CONSTRAINT ${fk.constraint_name}`;
|
291
|
-
ddl += ` FOREIGN KEY (${fk.column_name})`;
|
292
|
-
ddl += ` REFERENCES ${fk.foreign_table_name} (${fk.foreign_column_name})`;
|
293
|
-
if (fk.delete_rule && fk.delete_rule !== 'NO ACTION') {
|
294
|
-
ddl += ` ON DELETE ${fk.delete_rule}`;
|
295
|
-
}
|
296
|
-
if (fk.update_rule && fk.update_rule !== 'NO ACTION') {
|
297
|
-
ddl += ` ON UPDATE ${fk.update_rule}`;
|
298
|
-
}
|
299
|
-
ddl += ';';
|
300
|
-
}
|
301
|
-
// インデックス情報を取得
|
302
|
-
const indexResult = await client.query(`
|
303
|
-
SELECT
|
304
|
-
indexname,
|
305
|
-
indexdef
|
306
|
-
FROM pg_indexes
|
307
|
-
WHERE schemaname = 'public'
|
308
|
-
AND tablename = $1
|
309
|
-
AND indexname NOT LIKE '%_pkey'
|
310
|
-
AND indexname NOT IN (
|
311
|
-
SELECT tc.constraint_name
|
312
|
-
FROM information_schema.table_constraints tc
|
313
|
-
WHERE tc.table_name = $1 AND tc.constraint_type = 'UNIQUE'
|
314
|
-
)
|
315
|
-
ORDER BY indexname
|
316
|
-
`, [tableName]);
|
317
|
-
// インデックスを追加
|
318
|
-
for (const idx of indexResult.rows) {
|
319
|
-
ddl += `\n\n${idx.indexdef};`;
|
320
|
-
}
|
321
|
-
// DDLを元の形式で保存
|
191
|
+
// DDL生成をdefinitionExtractorの関数で統一
|
192
|
+
const ddl = await (0, definitionExtractor_1.generateCreateTableDDL)(client, tableName, 'public');
|
322
193
|
schemas[tableName] = {
|
323
194
|
ddl,
|
324
195
|
timestamp
|
package/package.json
CHANGED