@schema2sheet/cli 1.0.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/dist/cli.mjs +344 -0
- package/package.json +51 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { config } from "dotenv";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import { Parser } from "@dbml/core";
|
|
7
|
+
import { buildRefMap, collectEnumNames, convertDbmlTableToHeaders } from "@schema2sheet/dbml";
|
|
8
|
+
import { generateOas } from "@schema2sheet/openapi";
|
|
9
|
+
import { generateGas } from "@schema2sheet/gas";
|
|
10
|
+
import { google } from "googleapis";
|
|
11
|
+
|
|
12
|
+
//#region src/utils/parser.ts
|
|
13
|
+
function parseHeaderValue(raw) {
|
|
14
|
+
const match = raw.match(/^(.+?)(\*)?(?::(.+))?$/);
|
|
15
|
+
if (!match) return null;
|
|
16
|
+
const [, name, required] = match;
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
required: !!required,
|
|
20
|
+
raw
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/utils/helpers.ts
|
|
26
|
+
function escapeSheetName(name) {
|
|
27
|
+
if (name.includes(" ") || name.includes("'")) return `'${name.replace(/'/g, "''")}'`;
|
|
28
|
+
return name;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/commands/sheet-write.ts
|
|
33
|
+
const WRITE_SCOPES = ["https://www.googleapis.com/auth/spreadsheets"];
|
|
34
|
+
const DEFAULT_MANUAL_TEXT = `π μνΈ μ¬μ© μλ΄
|
|
35
|
+
|
|
36
|
+
β’ μ΄ ν(1ν)μ λ§€λ΄μΌμ
λλ€. μμ λ‘κ² μμ νμΈμ.
|
|
37
|
+
β’ 2νμ ν€λμ
λλ€. 컬λΌλͺ
κ³Ό νμ
μ΄ μ μλμ΄ μμ΅λλ€.
|
|
38
|
+
β’ 3νλΆν° λ°μ΄ν°λ₯Ό μ
λ ₯νμΈμ.
|
|
39
|
+
|
|
40
|
+
π ν€λ νμ: 컬λΌλͺ
*:νμ
|
|
41
|
+
β’ * (λ³ν): νμ μ
λ ₯ μ»¬λΌ (λΉμλ μ μμ)
|
|
42
|
+
β’ νμ
μ’
λ₯:
|
|
43
|
+
- string: μΌλ° ν
μ€νΈ
|
|
44
|
+
- uuid: κ³ μ μλ³μ (μ: 550e8400-e29b-41d4-a716-446655440000)
|
|
45
|
+
- email: μ΄λ©μΌ νμ (μ: user@example.com)
|
|
46
|
+
- uri: URL νμ (μ: https://example.com)
|
|
47
|
+
- date: λ μ§ (μ: 2024-01-15)
|
|
48
|
+
- date-time: λ μ§+μκ° (μ: 2024-01-15T09:30:00Z)
|
|
49
|
+
- number: μ«μ (μμμ κ°λ₯)
|
|
50
|
+
- integer: μ μ
|
|
51
|
+
- boolean: true λλ false
|
|
52
|
+
|
|
53
|
+
β οΈ μ£Όμμ¬ν
|
|
54
|
+
β’ id 컬λΌμ μμ νμ§ λ§μΈμ (μλ μμ±)
|
|
55
|
+
β’ νμ
μ΄λ *(νμ μ¬λΆ)λ₯Ό λ³κ²½νλ©΄ κΈ°μ‘΄ API λμμ΄ λ¬λΌμ§ μ μμ΅λλ€.
|
|
56
|
+
β’ ν€λλͺ
μ λ³κ²½ν κ²½μ°, λ°λμ OASλ₯Ό λ€μ μμ±νμΈμ: npx sheet-schema gen-oas`;
|
|
57
|
+
const DEFAULT_HEADER_COLOR = {
|
|
58
|
+
red: .71,
|
|
59
|
+
green: .84,
|
|
60
|
+
blue: .96
|
|
61
|
+
};
|
|
62
|
+
const BLACK_BORDER = {
|
|
63
|
+
style: "SOLID",
|
|
64
|
+
color: {
|
|
65
|
+
red: 0,
|
|
66
|
+
green: 0,
|
|
67
|
+
blue: 0
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function lightenColor(color, factor = .5) {
|
|
71
|
+
return {
|
|
72
|
+
red: color.red + (1 - color.red) * factor,
|
|
73
|
+
green: color.green + (1 - color.green) * factor,
|
|
74
|
+
blue: color.blue + (1 - color.blue) * factor
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function ensureSheetHeader(options) {
|
|
78
|
+
const auth = new google.auth.GoogleAuth({ scopes: WRITE_SCOPES });
|
|
79
|
+
const sheets = google.sheets({
|
|
80
|
+
version: "v4",
|
|
81
|
+
auth
|
|
82
|
+
});
|
|
83
|
+
const existing = (await sheets.spreadsheets.get({
|
|
84
|
+
spreadsheetId: options.spreadsheetId,
|
|
85
|
+
fields: "sheets(properties(sheetId,title),bandedRanges(bandedRangeId))"
|
|
86
|
+
})).data.sheets?.find((sheet) => sheet.properties?.title === options.sheetName);
|
|
87
|
+
let sheetId = existing?.properties?.sheetId;
|
|
88
|
+
let created = false;
|
|
89
|
+
if (sheetId == null && options.createIfMissing !== false) {
|
|
90
|
+
sheetId = (await sheets.spreadsheets.batchUpdate({
|
|
91
|
+
spreadsheetId: options.spreadsheetId,
|
|
92
|
+
requestBody: { requests: [{ addSheet: { properties: { title: options.sheetName } } }] }
|
|
93
|
+
})).data.replies?.[0]?.addSheet?.properties?.sheetId;
|
|
94
|
+
created = true;
|
|
95
|
+
}
|
|
96
|
+
if (sheetId == null) throw new Error(`Sheet not found: ${options.sheetName}`);
|
|
97
|
+
const range = `${escapeSheetName(options.sheetName)}!1:2`;
|
|
98
|
+
const existingRows = (await sheets.spreadsheets.values.get({
|
|
99
|
+
spreadsheetId: options.spreadsheetId,
|
|
100
|
+
range
|
|
101
|
+
})).data.values ?? [];
|
|
102
|
+
const hasValues = existingRows.some((row) => row?.some((value) => String(value ?? "").trim()));
|
|
103
|
+
if (hasValues && !options.force) throw new Error(`Sheet "${options.sheetName}" already has data in rows 1-2. Use --force to overwrite.`);
|
|
104
|
+
if (hasValues && options.force) {
|
|
105
|
+
await sheets.spreadsheets.values.clear({
|
|
106
|
+
spreadsheetId: options.spreadsheetId,
|
|
107
|
+
range
|
|
108
|
+
});
|
|
109
|
+
const bandings = existing?.bandedRanges ?? [];
|
|
110
|
+
if (bandings.length > 0) await sheets.spreadsheets.batchUpdate({
|
|
111
|
+
spreadsheetId: options.spreadsheetId,
|
|
112
|
+
requestBody: { requests: bandings.map((b) => ({ deleteBanding: { bandedRangeId: b.bandedRangeId } })) }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const manualText = options.manualText ?? DEFAULT_MANUAL_TEXT;
|
|
116
|
+
await sheets.spreadsheets.values.update({
|
|
117
|
+
spreadsheetId: options.spreadsheetId,
|
|
118
|
+
range: `${escapeSheetName(options.sheetName)}!A1`,
|
|
119
|
+
valueInputOption: "RAW",
|
|
120
|
+
requestBody: { values: [[manualText], options.headers] }
|
|
121
|
+
});
|
|
122
|
+
const headerColor = options.headerColor ?? DEFAULT_HEADER_COLOR;
|
|
123
|
+
const dataRowColor = lightenColor(headerColor, .6);
|
|
124
|
+
await sheets.spreadsheets.batchUpdate({
|
|
125
|
+
spreadsheetId: options.spreadsheetId,
|
|
126
|
+
requestBody: { requests: [
|
|
127
|
+
{ mergeCells: {
|
|
128
|
+
range: {
|
|
129
|
+
sheetId,
|
|
130
|
+
startRowIndex: 0,
|
|
131
|
+
endRowIndex: 1,
|
|
132
|
+
startColumnIndex: 0,
|
|
133
|
+
endColumnIndex: options.headers.length
|
|
134
|
+
},
|
|
135
|
+
mergeType: "MERGE_ALL"
|
|
136
|
+
} },
|
|
137
|
+
{ updateDimensionProperties: {
|
|
138
|
+
range: {
|
|
139
|
+
sheetId,
|
|
140
|
+
dimension: "ROWS",
|
|
141
|
+
startIndex: 0,
|
|
142
|
+
endIndex: 1
|
|
143
|
+
},
|
|
144
|
+
properties: { pixelSize: 200 },
|
|
145
|
+
fields: "pixelSize"
|
|
146
|
+
} },
|
|
147
|
+
{ repeatCell: {
|
|
148
|
+
range: {
|
|
149
|
+
sheetId,
|
|
150
|
+
startRowIndex: 0,
|
|
151
|
+
endRowIndex: 1,
|
|
152
|
+
startColumnIndex: 0,
|
|
153
|
+
endColumnIndex: options.headers.length
|
|
154
|
+
},
|
|
155
|
+
cell: { userEnteredFormat: {
|
|
156
|
+
wrapStrategy: "WRAP",
|
|
157
|
+
verticalAlignment: "TOP",
|
|
158
|
+
textFormat: { fontSize: 10 }
|
|
159
|
+
} },
|
|
160
|
+
fields: "userEnteredFormat(wrapStrategy,verticalAlignment,textFormat)"
|
|
161
|
+
} },
|
|
162
|
+
{ repeatCell: {
|
|
163
|
+
range: {
|
|
164
|
+
sheetId,
|
|
165
|
+
startRowIndex: 1,
|
|
166
|
+
endRowIndex: 2,
|
|
167
|
+
startColumnIndex: 0,
|
|
168
|
+
endColumnIndex: options.headers.length
|
|
169
|
+
},
|
|
170
|
+
cell: { userEnteredFormat: {
|
|
171
|
+
backgroundColor: headerColor,
|
|
172
|
+
textFormat: { bold: true },
|
|
173
|
+
horizontalAlignment: "CENTER",
|
|
174
|
+
verticalAlignment: "MIDDLE",
|
|
175
|
+
borders: {
|
|
176
|
+
top: BLACK_BORDER,
|
|
177
|
+
bottom: BLACK_BORDER,
|
|
178
|
+
left: BLACK_BORDER,
|
|
179
|
+
right: BLACK_BORDER
|
|
180
|
+
}
|
|
181
|
+
} },
|
|
182
|
+
fields: "userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment,borders)"
|
|
183
|
+
} },
|
|
184
|
+
{ addBanding: { bandedRange: {
|
|
185
|
+
range: {
|
|
186
|
+
sheetId,
|
|
187
|
+
startRowIndex: 2,
|
|
188
|
+
endRowIndex: 1e3,
|
|
189
|
+
startColumnIndex: 0,
|
|
190
|
+
endColumnIndex: options.headers.length
|
|
191
|
+
},
|
|
192
|
+
rowProperties: {
|
|
193
|
+
firstBandColor: {
|
|
194
|
+
red: 1,
|
|
195
|
+
green: 1,
|
|
196
|
+
blue: 1
|
|
197
|
+
},
|
|
198
|
+
secondBandColor: dataRowColor
|
|
199
|
+
}
|
|
200
|
+
} } },
|
|
201
|
+
{ updateBorders: {
|
|
202
|
+
range: {
|
|
203
|
+
sheetId,
|
|
204
|
+
startRowIndex: 1,
|
|
205
|
+
endRowIndex: 1e3,
|
|
206
|
+
startColumnIndex: 0,
|
|
207
|
+
endColumnIndex: options.headers.length
|
|
208
|
+
},
|
|
209
|
+
top: BLACK_BORDER,
|
|
210
|
+
bottom: BLACK_BORDER,
|
|
211
|
+
left: BLACK_BORDER,
|
|
212
|
+
right: BLACK_BORDER,
|
|
213
|
+
innerHorizontal: BLACK_BORDER,
|
|
214
|
+
innerVertical: BLACK_BORDER
|
|
215
|
+
} },
|
|
216
|
+
{ updateSheetProperties: {
|
|
217
|
+
properties: {
|
|
218
|
+
sheetId,
|
|
219
|
+
gridProperties: { frozenRowCount: 2 }
|
|
220
|
+
},
|
|
221
|
+
fields: "gridProperties.frozenRowCount"
|
|
222
|
+
} },
|
|
223
|
+
{ autoResizeDimensions: { dimensions: {
|
|
224
|
+
sheetId,
|
|
225
|
+
dimension: "COLUMNS",
|
|
226
|
+
startIndex: 0,
|
|
227
|
+
endIndex: options.headers.length
|
|
228
|
+
} } }
|
|
229
|
+
] }
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
sheetId,
|
|
233
|
+
created,
|
|
234
|
+
updated: true,
|
|
235
|
+
previousHeader: hasValues ? existingRows[1]?.map((value) => String(value ?? "")) : void 0
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/commands/apply.ts
|
|
241
|
+
async function applyDbml(options) {
|
|
242
|
+
const dbmlContent = await fs.readFile(options.filePath, "utf8");
|
|
243
|
+
const exported = new Parser().parse(dbmlContent, "dbml").export();
|
|
244
|
+
const refMap = buildRefMap(exported.schemas);
|
|
245
|
+
const enumNames = collectEnumNames(exported.schemas);
|
|
246
|
+
const tables = exported.schemas.flatMap((s) => s.tables);
|
|
247
|
+
if (tables.length === 0) {
|
|
248
|
+
console.error("No tables found in DBML file.");
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
for (const table of tables) {
|
|
252
|
+
const headers = convertDbmlTableToHeaders(table, refMap, enumNames);
|
|
253
|
+
if (!headers.some((h) => h.startsWith("id*"))) {
|
|
254
|
+
console.warn(`Table "${table.name}": no id* column found. Adding id*:uuid.`);
|
|
255
|
+
headers.unshift("id*:uuid");
|
|
256
|
+
}
|
|
257
|
+
for (const header of headers) if (!parseHeaderValue(header)) throw new Error(`Generated invalid header for table "${table.name}": ${header}`);
|
|
258
|
+
if (options.dryRun) {
|
|
259
|
+
console.log(`[dry-run] ${table.name}: ${headers.join(" | ")}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const result = await ensureSheetHeader({
|
|
263
|
+
spreadsheetId: options.spreadsheetId,
|
|
264
|
+
sheetName: table.name,
|
|
265
|
+
headers,
|
|
266
|
+
force: options.force,
|
|
267
|
+
createIfMissing: true
|
|
268
|
+
});
|
|
269
|
+
const action = result.created ? "Created" : "Updated";
|
|
270
|
+
console.log(`${action} sheet: ${table.name} (sheetId=${result.sheetId})`);
|
|
271
|
+
}
|
|
272
|
+
if (options.dryRun) {
|
|
273
|
+
console.log("\n[dry-run] No sheets were modified.");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
console.log("\nGenerating OpenAPI spec...");
|
|
277
|
+
await generateOas({
|
|
278
|
+
spreadsheetId: options.spreadsheetId,
|
|
279
|
+
title: "Spreadsheet API",
|
|
280
|
+
version: "0.1.0",
|
|
281
|
+
outputPath: "openapi.json",
|
|
282
|
+
schemaPath: "schema.json",
|
|
283
|
+
hashPath: "openapi.hash",
|
|
284
|
+
pathPrefix: "/api",
|
|
285
|
+
excludePrefixes: ["_", "#"],
|
|
286
|
+
includeHidden: false,
|
|
287
|
+
scanRows: 10,
|
|
288
|
+
headerRow: 2,
|
|
289
|
+
metadataKey: null,
|
|
290
|
+
enumLimit: 200,
|
|
291
|
+
debug: false
|
|
292
|
+
});
|
|
293
|
+
console.log("Generated: openapi.json, schema.json");
|
|
294
|
+
console.log("\nGenerating Google Apps Script...");
|
|
295
|
+
await generateGas({
|
|
296
|
+
schemaPath: "schema.json",
|
|
297
|
+
spreadsheetId: options.spreadsheetId,
|
|
298
|
+
outputPath: "api.gs"
|
|
299
|
+
});
|
|
300
|
+
if (options.deploy) {
|
|
301
|
+
console.log("\nDeploying to Google Apps Script...");
|
|
302
|
+
try {
|
|
303
|
+
execSync("clasp push", { stdio: "inherit" });
|
|
304
|
+
execSync("clasp deploy", { stdio: "inherit" });
|
|
305
|
+
console.log("Deployed successfully.");
|
|
306
|
+
} catch {
|
|
307
|
+
console.error("clasp deploy failed. Make sure clasp is installed and configured.");
|
|
308
|
+
console.error(" npm install -g @google/clasp");
|
|
309
|
+
console.error(" clasp login");
|
|
310
|
+
console.error(" clasp create --type webapp --rootDir .");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
console.log(`\nDone: ${tables.length} table(s) processed.`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/cli.ts
|
|
319
|
+
config();
|
|
320
|
+
const program = new Command();
|
|
321
|
+
program.name("sheet-schema").description("Apply DBML schema to Google Sheets and generate OpenAPI + GAS").version("1.0.0");
|
|
322
|
+
program.command("apply <file>").description("Apply a DBML schema: create sheets, generate OAS and GAS").option("-s, --spreadsheet-id <id>", "Spreadsheet ID (or set SPREADSHEET_ID env)").option("--force", "Overwrite existing header rows", false).option("--dry-run", "Print planned headers without writing to sheets", false).option("--deploy", "Deploy GAS to Apps Script via clasp", false).action(async (file, options) => {
|
|
323
|
+
const spreadsheetId = options.spreadsheetId || process.env.SPREADSHEET_ID;
|
|
324
|
+
if (!spreadsheetId) {
|
|
325
|
+
console.error("Missing spreadsheet id. Use --spreadsheet-id or SPREADSHEET_ID env.");
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
await applyDbml({
|
|
330
|
+
filePath: file,
|
|
331
|
+
spreadsheetId,
|
|
332
|
+
force: Boolean(options.force),
|
|
333
|
+
dryRun: Boolean(options.dryRun),
|
|
334
|
+
deploy: Boolean(options.deploy)
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error(error?.stack || error?.message || error);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
await program.parseAsync(process.argv);
|
|
342
|
+
|
|
343
|
+
//#endregion
|
|
344
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@schema2sheet/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for schema2sheet",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"schema2sheet": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cli",
|
|
15
|
+
"schema2sheet",
|
|
16
|
+
"google-sheets"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^11.1.0",
|
|
21
|
+
"dotenv": "^16.4.5",
|
|
22
|
+
"googleapis": "^140.0.0",
|
|
23
|
+
"@schema2sheet/openapi": "1.0.0",
|
|
24
|
+
"@schema2sheet/core": "1.0.0",
|
|
25
|
+
"@schema2sheet/gas": "1.0.0",
|
|
26
|
+
"@schema2sheet/sheets": "1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.2",
|
|
30
|
+
"typescript": "^5.7.2"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/froggy1014/schema2sheet.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/froggy1014/schema2sheet#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/froggy1014/schema2sheet/issues"
|
|
39
|
+
},
|
|
40
|
+
"author": "froggy1014",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"dev": "tsdown --watch",
|
|
47
|
+
"lint": "biome check --write .",
|
|
48
|
+
"clean": "rm -rf dist .turbo node_modules",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|