supatool 0.1.23 → 0.3.1

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.
@@ -1,50 +1,201 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
4
7
  // CLI entry point
5
8
  // Subcommand support with commander
6
9
  const commander_1 = require("commander");
7
10
  const index_1 = require("../index");
8
- const helptext_js_1 = require("./helptext.js"); // Import help text from external file
11
+ const helptext_1 = require("./helptext"); // Import help text from external file
9
12
  const package_json_1 = require("../../package.json");
13
+ const modelParser_1 = require("../parser/modelParser");
14
+ const docGenerator_1 = require("../generator/docGenerator");
15
+ const typeGenerator_1 = require("../generator/typeGenerator");
16
+ const crudGenerator_1 = require("../generator/crudGenerator");
17
+ const sqlGenerator_1 = require("../generator/sqlGenerator");
18
+ const rlsGenerator_1 = require("../generator/rlsGenerator");
19
+ const sync_1 = require("../sync");
20
+ const definitionExtractor_1 = require("../sync/definitionExtractor");
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const path_1 = __importDefault(require("path"));
10
23
  const program = new commander_1.Command();
11
24
  program
12
25
  .name('supatool')
13
26
  .description('Supatool CLI')
14
27
  .version(package_json_1.version);
15
- // crud subcommand
28
+ // extract command
16
29
  program
17
- .command('crud')
18
- .description('Generate CRUD types')
19
- .option('-i, --import <path>', 'Import path for type definitions', 'shared/')
20
- .option('-e, --export <path>', 'Output path for CRUD code', 'src/integrations/supabase/')
21
- .option('-t, --tables <tables>', 'Generate only for specific tables (comma separated)')
22
- .option('-f, --force', 'Force overwrite output folder without confirmation')
23
- .action((options) => {
24
- // Reflect command line arguments to process.argv (for main() reuse)
25
- const args = process.argv.slice(0, 2);
26
- if (options.import) {
27
- args.push('-i', options.import);
30
+ .command('extract')
31
+ .description('Extract and categorize database objects from Supabase')
32
+ .option('-c, --connection <string>', 'Supabase connection string')
33
+ .option('-o, --output-dir <path>', 'Output directory', './supabase/schemas')
34
+ .option('-t, --tables <pattern>', 'Table pattern with wildcards', '*')
35
+ .option('--tables-only', 'Extract only table definitions')
36
+ .option('--views-only', 'Extract only view definitions')
37
+ .option('--all', 'Extract all DB objects (tables, views, RLS, functions, triggers, cron, types)')
38
+ .option('--no-separate', 'Output all objects in same directory')
39
+ .option('--schema <schemas>', 'Target schemas, comma-separated (default: public)')
40
+ .option('--config <path>', 'Configuration file path')
41
+ .option('-f, --force', 'Force overwrite without confirmation')
42
+ .action(async (options) => {
43
+ const config = (0, sync_1.resolveConfig)({
44
+ connectionString: options.connection
45
+ }, options.config);
46
+ if (!config.connectionString) {
47
+ console.error('Connection string is required. Set it using one of:');
48
+ console.error('1. --connection option');
49
+ console.error('2. SUPABASE_CONNECTION_STRING environment variable');
50
+ console.error('3. DATABASE_URL environment variable');
51
+ console.error('4. supatool.config.json configuration file');
52
+ process.exit(1);
53
+ }
54
+ try {
55
+ // --schema オプションの処理
56
+ let schemas = ['public']; // デフォルト
57
+ if (options.schema) {
58
+ schemas = options.schema.split(',').map((s) => s.trim());
59
+ }
60
+ await (0, definitionExtractor_1.extractDefinitions)({
61
+ connectionString: config.connectionString,
62
+ outputDir: options.outputDir,
63
+ separateDirectories: options.separate !== false,
64
+ tablesOnly: options.tablesOnly,
65
+ viewsOnly: options.viewsOnly,
66
+ all: options.all,
67
+ tablePattern: options.tables,
68
+ force: options.force,
69
+ schemas: schemas
70
+ });
28
71
  }
29
- if (options.export) {
30
- args.push('-e', options.export);
72
+ catch (error) {
73
+ console.error('⚠️ Extraction error:', error);
74
+ process.exit(1);
31
75
  }
32
- if (options.force) {
33
- args.push('--force');
76
+ });
77
+ // config:init サブコマンド
78
+ program
79
+ .command('config:init')
80
+ .description('設定ファイル雛形を生成')
81
+ .option('-o, --out <path>', '出力先パス', 'supatool.config.json')
82
+ .action((options) => {
83
+ (0, sync_1.createConfigTemplate)(options.out);
84
+ });
85
+ // gen:types サブコマンド
86
+ program
87
+ .command('gen:types <modelPath>')
88
+ .description('モデルYAMLからTypeScript型定義を生成')
89
+ .option('-o, --out <path>', '出力先パス', 'docs/generated/types.ts')
90
+ .action((modelPath, options) => {
91
+ const model = (0, modelParser_1.parseModelYaml)(modelPath);
92
+ (0, typeGenerator_1.generateTypesFromModel)(model, options.out);
93
+ console.log('TypeScript型定義を出力:', options.out);
94
+ });
95
+ // gen:crud サブコマンド
96
+ program
97
+ .command('gen:crud <modelPath>')
98
+ .description('Generate CRUD TypeScript code from model YAML')
99
+ .option('-o, --out <dir>', '出力先ディレクトリ', 'docs/generated/crud')
100
+ .action((modelPath, options) => {
101
+ const model = (0, modelParser_1.parseModelYaml)(modelPath);
102
+ (0, crudGenerator_1.generateCrudFromModel)(model, options.out);
103
+ console.log('Generated CRUD TypeScript code:', options.out);
104
+ });
105
+ // gen:docs サブコマンド
106
+ program
107
+ .command('gen:docs <modelPath>')
108
+ .description('モデルYAMLからドキュメント(Markdown)を生成')
109
+ .option('-o, --out <path>', 'テーブル定義書出力先', 'docs/generated/table-doc.md')
110
+ .action((modelPath, options) => {
111
+ const model = (0, modelParser_1.parseModelYaml)(modelPath);
112
+ (0, docGenerator_1.generateTableDocMarkdown)(model, options.out);
113
+ (0, docGenerator_1.generateRelationsMarkdown)(model, 'docs/generated/relations.md');
114
+ console.log('テーブル定義書を出力:', options.out);
115
+ console.log('リレーション一覧を出力: docs/generated/relations.md');
116
+ });
117
+ // gen:sql サブコマンド
118
+ program
119
+ .command('gen:sql <modelPath>')
120
+ .description('モデルYAMLからテーブル・リレーション・RLS/セキュリティSQLを一括生成')
121
+ .option('-o, --out <path>', '出力先パス', 'docs/generated/schema.sql')
122
+ .action((modelPath, options) => {
123
+ const model = (0, modelParser_1.parseModelYaml)(modelPath);
124
+ // 一時ファイルに個別出力
125
+ const tmpSchema = 'docs/generated/.tmp_schema.sql';
126
+ const tmpRls = 'docs/generated/.tmp_rls.sql';
127
+ (0, sqlGenerator_1.generateSqlFromModel)(model, tmpSchema);
128
+ (0, rlsGenerator_1.generateRlsSqlFromModel)(model, tmpRls);
129
+ // 結合して1ファイルにまとめる
130
+ const schema = fs_1.default.readFileSync(tmpSchema, 'utf-8');
131
+ const rls = fs_1.default.readFileSync(tmpRls, 'utf-8');
132
+ fs_1.default.writeFileSync(options.out, schema + '\n' + rls);
133
+ fs_1.default.unlinkSync(tmpSchema);
134
+ fs_1.default.unlinkSync(tmpRls);
135
+ console.log('テーブル・リレーション・RLS/セキュリティSQLを一括出力:', options.out);
136
+ });
137
+ // gen:rls サブコマンド
138
+ program
139
+ .command('gen:rls <modelPath>')
140
+ .description('モデルYAMLからRLS/セキュリティポリシーSQLを生成')
141
+ .option('-o, --out <path>', '出力先パス', 'docs/generated/rls.sql')
142
+ .action((modelPath, options) => {
143
+ const model = (0, modelParser_1.parseModelYaml)(modelPath);
144
+ (0, rlsGenerator_1.generateRlsSqlFromModel)(model, options.out);
145
+ console.log('RLS/セキュリティポリシーSQLを出力:', options.out);
146
+ });
147
+ // gen:all サブコマンド
148
+ program
149
+ .command('gen:all <modelPath>')
150
+ .description('モデルYAMLから全て一括生成')
151
+ .action((modelPath) => {
152
+ const model = (0, modelParser_1.parseModelYaml)(modelPath);
153
+ (0, typeGenerator_1.generateTypesFromModel)(model, 'docs/generated/types.ts');
154
+ (0, crudGenerator_1.generateCrudFromModel)(model, 'docs/generated/crud');
155
+ (0, docGenerator_1.generateTableDocMarkdown)(model, 'docs/generated/table-doc.md');
156
+ (0, docGenerator_1.generateRelationsMarkdown)(model, 'docs/generated/relations.md');
157
+ console.log('TypeScript型定義を出力: docs/generated/types.ts');
158
+ console.log('CRUD関数TypeScriptコードを出力: docs/generated/crud/');
159
+ console.log('テーブル定義書を出力: docs/generated/table-doc.md');
160
+ console.log('リレーション一覧を出力: docs/generated/relations.md');
161
+ });
162
+ // create サブコマンド
163
+ program
164
+ .command('create <template>')
165
+ .description('テンプレートYAML雛形を生成')
166
+ .option('-o, --out <path>', '出力先パス', 'docs/model-schema-example.yaml')
167
+ .action((template, options) => {
168
+ const srcPath = path_1.default.join(__dirname, '../templates/yaml', `${template}.yaml`);
169
+ const destPath = options.out;
170
+ if (!fs_1.default.existsSync(srcPath)) {
171
+ console.error(`テンプレートが見つかりません: ${srcPath}`);
172
+ process.exit(1);
34
173
  }
35
- process.argv = args;
174
+ fs_1.default.copyFileSync(srcPath, destPath);
175
+ console.log(`テンプレート雛形を生成: ${destPath}`);
176
+ });
177
+ // crud コマンド(Supabase型定義→CRUD生成)
178
+ program
179
+ .command('crud')
180
+ .description('Supabase型定義(TypeScript)からCRUD関数TypeScriptコードを生成')
181
+ .option('-i, --input <path>', '型定義の入力パス', 'shared/')
182
+ .option('-o, --output <path>', 'CRUDコード出力先', 'src/integrations/supabase/')
183
+ .option('-t, --tables <names>', '生成対象テーブル(カンマ区切り)')
184
+ .option('-f, --force', '出力先を強制上書き')
185
+ .action((options) => {
186
+ // コマンドライン引数をmain()に渡すため、process.argvをそのまま利用
36
187
  (0, index_1.main)();
37
188
  });
38
- // help subcommand
189
+ // helpサブコマンド
39
190
  program
40
191
  .command('help')
41
192
  .description('Show help')
42
193
  .action(() => {
43
- console.log(helptext_js_1.helpText);
194
+ console.log(helptext_1.helpText);
44
195
  });
45
196
  // If no subcommand is specified, show helpText only (do not call main)
46
197
  if (!process.argv.slice(2).length) {
47
- console.log(helptext_js_1.helpText);
198
+ console.log(helptext_1.helpText);
48
199
  process.exit(0);
49
200
  }
50
201
  program.parse(process.argv);
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.supabase = void 0;
4
+ // Supabaseクライアント定義(自動生成/手動編集可)
5
+ // 日本語コメント
6
+ const supabase_js_1 = require("@supabase/supabase-js");
7
+ const supabaseUrl = process.env.SUPABASE_URL || '';
8
+ const supabaseKey = process.env.SUPABASE_KEY || '';
9
+ exports.supabase = (0, supabase_js_1.createClient)(supabaseUrl, supabaseKey);
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateCrudFromModel = generateCrudFromModel;
7
+ // CRUD関数TypeScriptコード自動生成(Supabase実動作対応)
8
+ // 日本語コメント
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ /**
12
+ * dataSchemaまたはmodels/tablesから各テーブルごとのCRUD関数TypeScriptファイルを生成
13
+ * @param model モデルオブジェクト
14
+ * @param outDir 出力先ディレクトリ
15
+ */
16
+ function generateCrudFromModel(model, outDir) {
17
+ if (!fs_1.default.existsSync(outDir)) {
18
+ fs_1.default.mkdirSync(outDir, { recursive: true });
19
+ }
20
+ let tables = [];
21
+ if (Array.isArray(model.dataSchema)) {
22
+ tables = model.dataSchema.map((t) => ({ tableName: t.tableName || t.raw, ...t }));
23
+ }
24
+ else if (Array.isArray(model.models)) {
25
+ for (const m of model.models) {
26
+ if (m.tables) {
27
+ for (const [tableName, table] of Object.entries(m.tables)) {
28
+ tables.push(Object.assign({ tableName }, table));
29
+ }
30
+ }
31
+ }
32
+ }
33
+ for (const tableObj of tables) {
34
+ if (tableObj.skipCreate)
35
+ continue;
36
+ const tableName = tableObj.tableName;
37
+ const capitalizedName = capitalize(tableName);
38
+ let code = `// 自動生成: ${tableName}用CRUD関数\n\n`;
39
+ code += `import { supabase } from '../client';\n`;
40
+ code += `import type { ${tableName} } from '../types';\n\n`;
41
+ // 型定義
42
+ code += `// フィルター型定義\n`;
43
+ code += `type FilterValue = string | number | boolean | null;\n`;
44
+ code += `type Filters = Record<string, FilterValue | FilterValue[]>;\n\n`;
45
+ // 全件取得関数(改良版)
46
+ code += `/** 全件取得 */\n`;
47
+ code += `export async function select${capitalizedName}Rows(): Promise<${tableName}[]> {\n`;
48
+ code += ` try {\n`;
49
+ code += ` const { data, error } = await supabase.from('${tableName}').select('*');\n`;
50
+ code += ` if (error) {\n`;
51
+ code += ` console.error('Error fetching all ${tableName}:', error);\n`;
52
+ code += ` throw new Error(\`Failed to fetch ${tableName}: \${error.message}\`);\n`;
53
+ code += ` }\n`;
54
+ code += ` if (!data) {\n`;
55
+ code += ` return [];\n`;
56
+ code += ` }\n`;
57
+ code += ` return data as ${tableName}[];\n`;
58
+ code += ` } catch (error) {\n`;
59
+ code += ` console.error('Unexpected error in select${capitalizedName}Rows:', error);\n`;
60
+ code += ` throw error;\n`;
61
+ code += ` }\n`;
62
+ code += `}\n\n`;
63
+ // IDで1件取得関数(改良版)
64
+ code += `/** IDで1件取得 */\n`;
65
+ code += `export async function select${capitalizedName}RowById({ id }: { id: string }): Promise<${tableName} | null> {\n`;
66
+ code += ` if (!id) {\n`;
67
+ code += ` throw new Error('ID is required');\n`;
68
+ code += ` }\n`;
69
+ code += ` try {\n`;
70
+ code += ` const { data, error } = await supabase.from('${tableName}').select('*').eq('id', id).single();\n`;
71
+ code += ` if (error) {\n`;
72
+ code += ` // レコードが見つからない場合(PGRST116)は null を返す\n`;
73
+ code += ` if (error.code === 'PGRST116') {\n`;
74
+ code += ` return null;\n`;
75
+ code += ` }\n`;
76
+ code += ` console.error('Error fetching ${tableName} by ID:', error);\n`;
77
+ code += ` throw new Error(\`Failed to fetch ${tableName} with ID \${id}: \${error.message}\`);\n`;
78
+ code += ` }\n`;
79
+ code += ` return data as ${tableName} | null;\n`;
80
+ code += ` } catch (error) {\n`;
81
+ code += ` console.error('Unexpected error in select${capitalizedName}RowById:', error);\n`;
82
+ code += ` throw error;\n`;
83
+ code += ` }\n`;
84
+ code += `}\n\n`;
85
+ // フィルターで検索関数
86
+ code += `/** フィルターで複数件取得 */\n`;
87
+ code += `export async function select${capitalizedName}RowsWithFilters({ filters }: { filters: Filters }): Promise<${tableName}[]> {\n`;
88
+ code += ` try {\n`;
89
+ code += ` let query = supabase.from('${tableName}').select('*');\n`;
90
+ code += ` \n`;
91
+ code += ` // フィルターを適用\n`;
92
+ code += ` for (const [key, value] of Object.entries(filters)) {\n`;
93
+ code += ` if (Array.isArray(value)) {\n`;
94
+ code += ` query = query.in(key, value);\n`;
95
+ code += ` } else {\n`;
96
+ code += ` query = query.eq(key, value);\n`;
97
+ code += ` }\n`;
98
+ code += ` }\n`;
99
+ code += ` \n`;
100
+ code += ` const { data, error } = await query;\n`;
101
+ code += ` if (error) {\n`;
102
+ code += ` console.error('Error fetching ${tableName} by filters:', error);\n`;
103
+ code += ` throw new Error(\`Failed to fetch ${tableName}: \${error.message}\`);\n`;
104
+ code += ` }\n`;
105
+ code += ` return (data as unknown as ${tableName}[]) || [];\n`;
106
+ code += ` } catch (error) {\n`;
107
+ code += ` console.error('Unexpected error in select${capitalizedName}RowsWithFilters:', error);\n`;
108
+ code += ` throw error;\n`;
109
+ code += ` }\n`;
110
+ code += `}\n\n`;
111
+ // 作成関数
112
+ code += `/** 新規作成 */\n`;
113
+ code += `export async function insert${capitalizedName}Row({ data }: { data: Omit<${tableName}, 'id' | 'created_at' | 'updated_at'> }): Promise<${tableName}> {\n`;
114
+ code += ` if (!data) {\n`;
115
+ code += ` throw new Error('Data is required for creation');\n`;
116
+ code += ` }\n`;
117
+ code += ` try {\n`;
118
+ code += ` const { data: createdData, error } = await supabase\n`;
119
+ code += ` .from('${tableName}')\n`;
120
+ code += ` .insert([data])\n`;
121
+ code += ` .select()\n`;
122
+ code += ` .single();\n`;
123
+ code += ` if (error) {\n`;
124
+ code += ` console.error('Error creating ${tableName}:', error);\n`;
125
+ code += ` throw new Error(\`Failed to create ${tableName}: \${error.message}\`);\n`;
126
+ code += ` }\n`;
127
+ code += ` if (!createdData) {\n`;
128
+ code += ` throw new Error('No data returned after creation');\n`;
129
+ code += ` }\n`;
130
+ code += ` return createdData as ${tableName};\n`;
131
+ code += ` } catch (error) {\n`;
132
+ code += ` console.error('Unexpected error in insert${capitalizedName}Row:', error);\n`;
133
+ code += ` throw error;\n`;
134
+ code += ` }\n`;
135
+ code += `}\n\n`;
136
+ // 更新関数
137
+ code += `/** 更新 */\n`;
138
+ code += `export async function update${capitalizedName}Row({ id, data }: { id: string; data: Partial<Omit<${tableName}, 'id' | 'created_at'>> }): Promise<${tableName}> {\n`;
139
+ code += ` if (!id) {\n`;
140
+ code += ` throw new Error('ID is required for update');\n`;
141
+ code += ` }\n`;
142
+ code += ` if (!data || Object.keys(data).length === 0) {\n`;
143
+ code += ` throw new Error('Update data is required');\n`;
144
+ code += ` }\n`;
145
+ code += ` try {\n`;
146
+ code += ` const { data: updatedData, error } = await supabase\n`;
147
+ code += ` .from('${tableName}')\n`;
148
+ code += ` .update(data)\n`;
149
+ code += ` .eq('id', id)\n`;
150
+ code += ` .select()\n`;
151
+ code += ` .single();\n`;
152
+ code += ` if (error) {\n`;
153
+ code += ` if (error.code === 'PGRST116') {\n`;
154
+ code += ` throw new Error(\`${tableName} with ID \${id} not found\`);\n`;
155
+ code += ` }\n`;
156
+ code += ` console.error('Error updating ${tableName}:', error);\n`;
157
+ code += ` throw new Error(\`Failed to update ${tableName} with ID \${id}: \${error.message}\`);\n`;
158
+ code += ` }\n`;
159
+ code += ` if (!updatedData) {\n`;
160
+ code += ` throw new Error(\`${tableName} with ID \${id} not found\`);\n`;
161
+ code += ` }\n`;
162
+ code += ` return updatedData as ${tableName};\n`;
163
+ code += ` } catch (error) {\n`;
164
+ code += ` console.error('Unexpected error in update${capitalizedName}Row:', error);\n`;
165
+ code += ` throw error;\n`;
166
+ code += ` }\n`;
167
+ code += `}\n\n`;
168
+ // 削除関数
169
+ code += `/** 削除 */\n`;
170
+ code += `export async function delete${capitalizedName}Row({ id }: { id: string }): Promise<boolean> {\n`;
171
+ code += ` if (!id) {\n`;
172
+ code += ` throw new Error('ID is required for deletion');\n`;
173
+ code += ` }\n`;
174
+ code += ` try {\n`;
175
+ code += ` const { error } = await supabase\n`;
176
+ code += ` .from('${tableName}')\n`;
177
+ code += ` .delete()\n`;
178
+ code += ` .eq('id', id);\n`;
179
+ code += ` if (error) {\n`;
180
+ code += ` console.error('Error deleting ${tableName}:', error);\n`;
181
+ code += ` throw new Error(\`Failed to delete ${tableName} with ID \${id}: \${error.message}\`);\n`;
182
+ code += ` }\n`;
183
+ code += ` return true;\n`;
184
+ code += ` } catch (error) {\n`;
185
+ code += ` console.error('Unexpected error in delete${capitalizedName}Row:', error);\n`;
186
+ code += ` throw error;\n`;
187
+ code += ` }\n`;
188
+ code += `}\n\n`;
189
+ fs_1.default.writeFileSync(path_1.default.join(outDir, `${tableName}.ts`), code);
190
+ }
191
+ }
192
+ function capitalize(str) {
193
+ return str.charAt(0).toUpperCase() + str.slice(1);
194
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateTableDocMarkdown = generateTableDocMarkdown;
7
+ exports.generateRelationsMarkdown = generateRelationsMarkdown;
8
+ // ドキュメント生成(最小雛形)
9
+ // 日本語コメント
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ /**
13
+ * テーブル定義書(Markdown)を生成して保存
14
+ * @param model モデルオブジェクト
15
+ * @param outPath 出力先パス
16
+ */
17
+ function generateTableDocMarkdown(model, outPath) {
18
+ // 出力先ディレクトリを作成
19
+ const dir = path_1.default.dirname(outPath);
20
+ if (!fs_1.default.existsSync(dir)) {
21
+ fs_1.default.mkdirSync(dir, { recursive: true });
22
+ }
23
+ let md = '# テーブル定義書\n\n';
24
+ for (const m of model.models) {
25
+ const tables = m.tables || {};
26
+ for (const [tableName, table] of Object.entries(tables)) {
27
+ const t = table;
28
+ let skipNote = t.skipCreate ? '(作成不要: Supabase組み込み)' : '';
29
+ md += `## ${tableName}${skipNote}\n`;
30
+ if (t.description)
31
+ md += `${t.description}\n`;
32
+ md += '\n| カラム | 型 | 主キー | NotNull | デフォルト | ラベル |\n|---|---|---|---|---|---|\n';
33
+ for (const [colName, col] of Object.entries(t.fields || {})) {
34
+ const c = col;
35
+ md += `| ${colName} | ${c.type || ''} | ${c.primary ? '★' : ''} | ${c.notNull ? '○' : ''} | ${c.default || ''} | ${c.label || ''} |\n`;
36
+ }
37
+ md += '\n';
38
+ }
39
+ }
40
+ fs_1.default.writeFileSync(outPath, md);
41
+ }
42
+ /**
43
+ * リレーション一覧(Markdown)を生成して保存
44
+ * @param model モデルオブジェクト
45
+ * @param outPath 出力先パス
46
+ */
47
+ function generateRelationsMarkdown(model, outPath) {
48
+ const dir = path_1.default.dirname(outPath);
49
+ if (!fs_1.default.existsSync(dir)) {
50
+ fs_1.default.mkdirSync(dir, { recursive: true });
51
+ }
52
+ let md = '# リレーション一覧\n\n| テーブル | 関係 | 対象 | 外部キー |\n|---|---|---|---|\n';
53
+ for (const m of model.models) {
54
+ const tables = m.tables || {};
55
+ for (const [tableName, table] of Object.entries(tables)) {
56
+ const t = table;
57
+ for (const [relName, rel] of Object.entries(t.relations || {})) {
58
+ const r = rel;
59
+ md += `| ${tableName} | ${r.type || ''} | ${r.target || ''} | ${r.foreignKey || ''} |\n`;
60
+ }
61
+ }
62
+ }
63
+ fs_1.default.writeFileSync(outPath, md);
64
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateRlsSqlFromModel = generateRlsSqlFromModel;
7
+ // RLS/セキュリティポリシーSQL自動生成(最小雛形)
8
+ // Todo: スキーマを設定可能とする
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ /**
12
+ * モデルからRLS/セキュリティポリシーSQLを生成
13
+ * @param model モデルオブジェクト
14
+ * @param outPath 出力先パス
15
+ */
16
+ function generateRlsSqlFromModel(model, outPath) {
17
+ const dir = path_1.default.dirname(outPath);
18
+ if (!fs_1.default.existsSync(dir)) {
19
+ fs_1.default.mkdirSync(dir, { recursive: true });
20
+ }
21
+ // dataSchema/models両対応: テーブル一覧取得
22
+ let tables = [];
23
+ if (Array.isArray(model.dataSchema)) {
24
+ tables = model.dataSchema.map((t) => ({ tableName: t.tableName || t.raw, ...t }));
25
+ }
26
+ else if (Array.isArray(model.models)) {
27
+ for (const m of model.models) {
28
+ if (m.tables) {
29
+ for (const [tableName, table] of Object.entries(m.tables)) {
30
+ tables.push(Object.assign({ tableName }, table));
31
+ }
32
+ }
33
+ }
34
+ }
35
+ let sql = '-- 自動生成: RLS/セキュリティポリシーDDL\n\n';
36
+ const security = model.security || {};
37
+ // roles定義があればロールマスタ・ユーザーロールDDLも自動生成
38
+ if (model.roles && Array.isArray(model.roles)) {
39
+ sql += '-- ロールマスタ・ユーザーロールDDL(自動生成)\n';
40
+ sql += `CREATE TABLE IF NOT EXISTS m_roles (\n id uuid PRIMARY KEY,\n name text NOT NULL\n);\n\n`;
41
+ sql += `CREATE TABLE IF NOT EXISTS user_roles (\n id uuid PRIMARY KEY,\n user_id uuid NOT NULL,\n role_id uuid NOT NULL\n);\n\n`;
42
+ for (const role of model.roles) {
43
+ sql += `INSERT INTO m_roles (id, name) VALUES (gen_random_uuid(), '${role}') ON CONFLICT DO NOTHING;\n`;
44
+ }
45
+ sql += '\n';
46
+ }
47
+ // DBファンクション
48
+ if (security.functions) {
49
+ for (const [fnName, fn] of Object.entries(security.functions)) {
50
+ const f = fn;
51
+ const useTemplate = f.use_template !== false; // デフォルトtrue
52
+ const templateType = f.template_type || 'simple';
53
+ let sqlBody = f.sql;
54
+ if (!sqlBody && useTemplate) {
55
+ // template_type優先
56
+ if (templateType) {
57
+ sqlBody = getFunctionTemplate(fnName, templateType, model);
58
+ }
59
+ }
60
+ // fallback: roles定義があればuser_roles参照テンプレ
61
+ if (!sqlBody) {
62
+ if (model.roles && Array.isArray(model.roles)) {
63
+ sqlBody = `CREATE FUNCTION ${fnName}() RETURNS text AS $$\nBEGIN\n -- ログインユーザーのロールを返す(テンプレ)\n RETURN (SELECT r.name FROM user_roles ur JOIN m_roles r ON ur.role_id = r.id WHERE ur.user_id = current_setting('request.jwt.claim.sub', true)::uuid LIMIT 1);\nEND;\n$$ LANGUAGE plpgsql;`;
64
+ }
65
+ else {
66
+ sqlBody = `CREATE FUNCTION ${fnName}() RETURNS text AS $$\nBEGIN\n -- TODO: ロジックを記述\n RETURN 'admin';\nEND;\n$$ LANGUAGE plpgsql;`;
67
+ }
68
+ }
69
+ sql += `-- ${fnName}関数: ユーザーのロール取得\n`;
70
+ sql += `${sqlBody}\n\n`;
71
+ }
72
+ }
73
+ // RLSポリシー
74
+ if (security.policies) {
75
+ for (const [tableName, tablePolicies] of Object.entries(security.policies)) {
76
+ const p = tablePolicies;
77
+ for (const [action, policy] of Object.entries(p)) {
78
+ const pol = policy;
79
+ const useTemplate = pol.use_template !== false; // デフォルトtrue
80
+ const templateType = pol.template_type || 'simple';
81
+ let usingCond = pol.using;
82
+ if (!usingCond && useTemplate && pol.role && Array.isArray(pol.role)) {
83
+ usingCond = getPolicyTemplate(pol.role, templateType);
84
+ }
85
+ if (!usingCond) {
86
+ usingCond = 'true -- TODO: 適切な条件を記述';
87
+ }
88
+ sql += `-- ${tableName}テーブル ${action}用RLSポリシー\n`;
89
+ sql += `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;\n`;
90
+ sql += `CREATE POLICY ${tableName}_${action}_policy ON ${tableName}\n FOR ${action.toUpperCase()}\n USING (${usingCond});\n\n`;
91
+ }
92
+ }
93
+ }
94
+ fs_1.default.writeFileSync(outPath, sql);
95
+ }
96
+ // テンプレート切り替え関数
97
+ function getFunctionTemplate(fnName, type, model) {
98
+ const templatePath = path_1.default.join(__dirname, '../templates/rls', `function_${type}.sql`);
99
+ let template = fs_1.default.readFileSync(templatePath, 'utf-8');
100
+ template = template.replace(/\$\{functionName\}/g, fnName);
101
+ template = template.replace(/\$\{userIdExpr\}/g, "current_setting('request.jwt.claim.sub', true)::uuid");
102
+ template = template.replace(/\$\{tenantIdExpr\}/g, "current_setting('tenant.id', true)::uuid");
103
+ return template;
104
+ }
105
+ function getPolicyTemplate(roles, type) {
106
+ const templatePath = path_1.default.join(__dirname, '../templates/rls', `policy_${type}.sql`);
107
+ let template = fs_1.default.readFileSync(templatePath, 'utf-8');
108
+ template = template.replace(/\$\{roles\}/g, roles.map(r => `'${r}'`).join(', '));
109
+ template = template.replace(/\$\{tenantIdExpr\}/g, "current_setting('tenant.id', true)::uuid");
110
+ return template;
111
+ }