nsa-sheets-db-builder 4.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/bin/sheets-deployer.mjs +169 -0
  4. package/libs/alasql.js +15577 -0
  5. package/libs/common/gas_response_helper.ts +147 -0
  6. package/libs/common/gaserror.ts +101 -0
  7. package/libs/common/gaslogger.ts +172 -0
  8. package/libs/db_ddl.ts +316 -0
  9. package/libs/libraries.json +56 -0
  10. package/libs/spreadsheets_db.ts +4406 -0
  11. package/libs/triggers.ts +113 -0
  12. package/package.json +73 -0
  13. package/scripts/build.mjs +513 -0
  14. package/scripts/clean.mjs +31 -0
  15. package/scripts/create.mjs +94 -0
  16. package/scripts/ddl-handler.mjs +232 -0
  17. package/scripts/describe.mjs +38 -0
  18. package/scripts/drop.mjs +39 -0
  19. package/scripts/init.mjs +465 -0
  20. package/scripts/lib/utils.mjs +1019 -0
  21. package/scripts/login.mjs +102 -0
  22. package/scripts/provision.mjs +35 -0
  23. package/scripts/refresh-cache.mjs +34 -0
  24. package/scripts/set-key.mjs +48 -0
  25. package/scripts/setup-trigger.mjs +95 -0
  26. package/scripts/setup.mjs +677 -0
  27. package/scripts/show.mjs +37 -0
  28. package/scripts/sync.mjs +35 -0
  29. package/scripts/whoami.mjs +36 -0
  30. package/src/api/ddl-handler-entry.ts +136 -0
  31. package/src/api/ddl.ts +321 -0
  32. package/src/templates/.clasp.json.ejs +1 -0
  33. package/src/templates/appsscript.json.ejs +16 -0
  34. package/src/templates/config.ts.ejs +14 -0
  35. package/src/templates/ddl-handler-config.ts.ejs +3 -0
  36. package/src/templates/ddl-handler-main.ts.ejs +56 -0
  37. package/src/templates/main.ts.ejs +288 -0
  38. package/src/templates/rbac.ts.ejs +148 -0
  39. package/src/templates/views.ts.ejs +92 -0
  40. package/templates/blank.json +33 -0
  41. package/templates/blog-cms.json +507 -0
  42. package/templates/crm.json +360 -0
  43. package/templates/e-commerce.json +424 -0
  44. package/templates/inventory.json +307 -0
package/libs/db_ddl.ts ADDED
@@ -0,0 +1,316 @@
1
+ /**
2
+ * DB DDL V2 - Schema provisioning (DDL) for Google Sheets databases
3
+ *
4
+ * Provides CREATE TABLE / SHOW TABLES / DESCRIBE / DROP TABLE / SYNC SCHEMA
5
+ * operations for Google Sheets, treating sheets as tables and row 1 as headers.
6
+ *
7
+ * Usage via clasp run:
8
+ * clasp run ddlCreateSpreadsheet --params '[{"name":"my-db"}]'
9
+ * clasp run ddlProvisionTables --params '[{"spreadsheetId":"...","tables":{...}}]'
10
+ * clasp run ddlShowTables --params '[{"spreadsheetId":"..."}]'
11
+ * clasp run ddlDescribeTable --params '[{"spreadsheetId":"...","table":"users"}]'
12
+ * clasp run ddlDropTable --params '[{"spreadsheetId":"...","table":"users"}]'
13
+ * clasp run ddlSyncSchema --params '[{"spreadsheetId":"...","tables":{...}}]'
14
+ */
15
+
16
+ // Declare dependencies
17
+ declare class GASLoggerV2 {
18
+ info(source: string, message: string, context: any): void;
19
+ warn(source: string, message: string, context: any): void;
20
+ debug(source: string, message: string, context: any): void;
21
+ }
22
+
23
+ // ── Constants ─────────────────────────────
24
+
25
+ const DDL_SYS_TABLES_SHEET = '__sys__tables__';
26
+ const DDL_SYS_TABLES_HEADERS = ['table_name', 'sheet_name', 'spreadsheet_id', 'created_at', 'column_count'];
27
+
28
+ // ── Types ─────────────────────────────────
29
+
30
+ interface DDLCreateSpreadsheetResult {
31
+ spreadsheetId: string;
32
+ url: string;
33
+ name: string;
34
+ }
35
+
36
+ interface DDLTableResult {
37
+ name: string;
38
+ sheetName: string;
39
+ spreadsheetId: string;
40
+ status: 'created' | 'exists' | 'skipped';
41
+ columns?: number;
42
+ reason?: string;
43
+ }
44
+
45
+ interface DDLProvisionResult {
46
+ tables: DDLTableResult[];
47
+ }
48
+
49
+ interface DDLShowTablesEntry {
50
+ name: string;
51
+ rows: number;
52
+ columns: number;
53
+ }
54
+
55
+ interface DDLDescribeResult {
56
+ table: string;
57
+ columns: string[];
58
+ rowCount: number;
59
+ }
60
+
61
+ interface DDLSyncResult {
62
+ created: { name: string; sheetName: string; columns: number }[];
63
+ existing: { name: string; sheetName: string }[];
64
+ drift: { name: string; sheetName: string; missing: string[]; extra: string[] }[];
65
+ }
66
+
67
+ // ── Core DDL Class ────────────────────────
68
+
69
+ class DBDDLV2 {
70
+ private logger: GASLoggerV2 | null;
71
+
72
+ constructor(logger?: GASLoggerV2 | null) {
73
+ this.logger = logger || null;
74
+ }
75
+
76
+ /**
77
+ * Create a new Google Spreadsheet and initialize it with __sys__tables__
78
+ */
79
+ createSpreadsheet(name: string): DDLCreateSpreadsheetResult {
80
+ if (!name) throw new Error('Missing "name" parameter');
81
+
82
+ const ss = SpreadsheetApp.create(name);
83
+ const spreadsheetId = ss.getId();
84
+ const url = ss.getUrl();
85
+
86
+ // Initialize sys tables, then remove default Sheet1
87
+ this.ensureSysTablesSheet(ss);
88
+ const defaultSheet = ss.getSheetByName('Sheet1');
89
+ if (defaultSheet) {
90
+ ss.deleteSheet(defaultSheet);
91
+ }
92
+
93
+ this.logger?.info('DBDDLV2.createSpreadsheet', `Created spreadsheet "${name}"`, { spreadsheetId });
94
+
95
+ return { spreadsheetId, url, name };
96
+ }
97
+
98
+ /**
99
+ * Provision tables from a DBConfigV2-shaped tables object.
100
+ * Creates sheets with headers + __sys__tables__ entries for each table.
101
+ */
102
+ provisionTables(spreadsheetId: string, tables: Record<string, any>): DDLProvisionResult {
103
+ if (!spreadsheetId) throw new Error('Missing "spreadsheetId" parameter');
104
+ if (!tables || typeof tables !== 'object') throw new Error('Missing or invalid "tables" parameter');
105
+
106
+ const ss = SpreadsheetApp.openById(spreadsheetId);
107
+ this.ensureSysTablesSheet(ss);
108
+
109
+ const results: DDLTableResult[] = [];
110
+
111
+ for (const [tableName, tableConfig] of Object.entries(tables)) {
112
+ const sheetName = tableConfig.sheetName || tableName;
113
+ const schema = tableConfig.schema;
114
+
115
+ if (!schema) {
116
+ this.logger?.warn('DBDDLV2.provisionTables', `Table "${tableName}" has no schema, skipping`, {});
117
+ results.push({ name: tableName, sheetName, spreadsheetId, status: 'skipped', reason: 'no schema' });
118
+ continue;
119
+ }
120
+
121
+ let sheet = ss.getSheetByName(sheetName);
122
+ if (sheet) {
123
+ this.logger?.info('DBDDLV2.provisionTables', `Sheet "${sheetName}" already exists, skipping`, {});
124
+ results.push({ name: tableName, sheetName, spreadsheetId, status: 'exists' });
125
+ continue;
126
+ }
127
+
128
+ // Create sheet with headers
129
+ sheet = ss.insertSheet(sheetName);
130
+ const headers = Object.keys(schema);
131
+ sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
132
+ sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
133
+ sheet.setFrozenRows(1);
134
+
135
+ // Register in __sys__tables__
136
+ this.registerInSysTables(ss, tableName, sheetName, spreadsheetId, headers.length);
137
+
138
+ this.logger?.info('DBDDLV2.provisionTables', `Created sheet "${sheetName}" with ${headers.length} columns`, {});
139
+ results.push({ name: tableName, sheetName, spreadsheetId, status: 'created', columns: headers.length });
140
+ }
141
+
142
+ return { tables: results };
143
+ }
144
+
145
+ /**
146
+ * List all sheets in a spreadsheet with row/column counts
147
+ */
148
+ showTables(spreadsheetId: string): { tables: DDLShowTablesEntry[] } {
149
+ if (!spreadsheetId) throw new Error('Missing "spreadsheetId" parameter');
150
+
151
+ const ss = SpreadsheetApp.openById(spreadsheetId);
152
+ const sheets = ss.getSheets();
153
+
154
+ const tables = sheets.map(sheet => {
155
+ const name = sheet.getName();
156
+ const rows = Math.max(0, sheet.getLastRow() - 1);
157
+ const columns = sheet.getLastColumn();
158
+ return { name, rows, columns };
159
+ });
160
+
161
+ return { tables };
162
+ }
163
+
164
+ /**
165
+ * Describe a specific table — returns headers and row count
166
+ */
167
+ describeTable(spreadsheetId: string, table: string): DDLDescribeResult {
168
+ if (!spreadsheetId) throw new Error('Missing "spreadsheetId" parameter');
169
+ if (!table) throw new Error('Missing "table" parameter');
170
+
171
+ const ss = SpreadsheetApp.openById(spreadsheetId);
172
+ const sheet = ss.getSheetByName(table);
173
+
174
+ if (!sheet) {
175
+ throw new Error(`Sheet "${table}" not found`);
176
+ }
177
+
178
+ const lastCol = sheet.getLastColumn();
179
+ if (lastCol === 0) {
180
+ return { table, columns: [], rowCount: 0 };
181
+ }
182
+
183
+ const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0].map(String);
184
+ const rowCount = Math.max(0, sheet.getLastRow() - 1);
185
+
186
+ return { table, columns: headers, rowCount };
187
+ }
188
+
189
+ /**
190
+ * Drop a table (delete the sheet) and unregister from __sys__tables__
191
+ */
192
+ dropTable(spreadsheetId: string, table: string): { dropped: string } {
193
+ if (!spreadsheetId) throw new Error('Missing "spreadsheetId" parameter');
194
+ if (!table) throw new Error('Missing "table" parameter');
195
+
196
+ if (table === DDL_SYS_TABLES_SHEET) {
197
+ throw new Error('Cannot drop system metadata sheet');
198
+ }
199
+
200
+ const ss = SpreadsheetApp.openById(spreadsheetId);
201
+ const sheet = ss.getSheetByName(table);
202
+
203
+ if (!sheet) {
204
+ throw new Error(`Sheet "${table}" not found`);
205
+ }
206
+
207
+ this.unregisterFromSysTables(ss, table);
208
+ ss.deleteSheet(sheet);
209
+
210
+ this.logger?.info('DBDDLV2.dropTable', `Dropped sheet "${table}"`, { spreadsheetId });
211
+
212
+ return { dropped: table };
213
+ }
214
+
215
+ /**
216
+ * Sync schema — provisions missing tables, reports drift on existing ones
217
+ */
218
+ syncSchema(spreadsheetId: string, tables: Record<string, any>): DDLSyncResult {
219
+ if (!spreadsheetId) throw new Error('Missing "spreadsheetId" parameter');
220
+ if (!tables || typeof tables !== 'object') throw new Error('Missing or invalid "tables" parameter');
221
+
222
+ const ss = SpreadsheetApp.openById(spreadsheetId);
223
+ this.ensureSysTablesSheet(ss);
224
+
225
+ const created: DDLSyncResult['created'] = [];
226
+ const existing: DDLSyncResult['existing'] = [];
227
+ const drift: DDLSyncResult['drift'] = [];
228
+
229
+ for (const [tableName, tableConfig] of Object.entries(tables)) {
230
+ const sheetName = tableConfig.sheetName || tableName;
231
+ const schema = tableConfig.schema;
232
+ if (!schema) continue;
233
+
234
+ const expectedHeaders = Object.keys(schema);
235
+ const sheet = ss.getSheetByName(sheetName);
236
+
237
+ if (!sheet) {
238
+ // Create missing sheet
239
+ const newSheet = ss.insertSheet(sheetName);
240
+ newSheet.getRange(1, 1, 1, expectedHeaders.length).setValues([expectedHeaders]);
241
+ newSheet.getRange(1, 1, 1, expectedHeaders.length).setFontWeight('bold');
242
+ newSheet.setFrozenRows(1);
243
+ this.registerInSysTables(ss, tableName, sheetName, spreadsheetId, expectedHeaders.length);
244
+
245
+ created.push({ name: tableName, sheetName, columns: expectedHeaders.length });
246
+ continue;
247
+ }
248
+
249
+ // Sheet exists — check for drift
250
+ const lastCol = sheet.getLastColumn();
251
+ const actualHeaders = lastCol > 0
252
+ ? sheet.getRange(1, 1, 1, lastCol).getValues()[0].map(String)
253
+ : [];
254
+
255
+ const missing = expectedHeaders.filter(h => !actualHeaders.includes(h));
256
+ const extra = actualHeaders.filter(h => !expectedHeaders.includes(h));
257
+
258
+ if (missing.length > 0 || extra.length > 0) {
259
+ drift.push({ name: tableName, sheetName, missing, extra });
260
+ } else {
261
+ existing.push({ name: tableName, sheetName });
262
+ }
263
+ }
264
+
265
+ return { created, existing, drift };
266
+ }
267
+
268
+ // ── Internal helpers ──────────────────────
269
+
270
+ private ensureSysTablesSheet(ss: GoogleAppsScript.Spreadsheet.Spreadsheet): GoogleAppsScript.Spreadsheet.Sheet {
271
+ let sheet = ss.getSheetByName(DDL_SYS_TABLES_SHEET);
272
+ if (sheet) return sheet;
273
+
274
+ sheet = ss.insertSheet(DDL_SYS_TABLES_SHEET);
275
+ sheet.getRange(1, 1, 1, DDL_SYS_TABLES_HEADERS.length).setValues([DDL_SYS_TABLES_HEADERS]);
276
+ sheet.getRange(1, 1, 1, DDL_SYS_TABLES_HEADERS.length).setFontWeight('bold');
277
+ sheet.setFrozenRows(1);
278
+
279
+ this.logger?.info('DBDDLV2.ensureSysTablesSheet', 'Created __sys__tables__ metadata sheet', {});
280
+
281
+ return sheet;
282
+ }
283
+
284
+ private registerInSysTables(
285
+ ss: GoogleAppsScript.Spreadsheet.Spreadsheet,
286
+ tableName: string,
287
+ sheetName: string,
288
+ spreadsheetId: string,
289
+ columnCount: number
290
+ ): void {
291
+ const sysSheet = ss.getSheetByName(DDL_SYS_TABLES_SHEET);
292
+ if (!sysSheet) return;
293
+
294
+ const row = [tableName, sheetName, spreadsheetId, new Date().toISOString(), columnCount];
295
+ sysSheet.appendRow(row);
296
+
297
+ this.logger?.info('DBDDLV2.registerInSysTables', `Registered "${tableName}" in __sys__tables__`, {});
298
+ }
299
+
300
+ private unregisterFromSysTables(
301
+ ss: GoogleAppsScript.Spreadsheet.Spreadsheet,
302
+ tableName: string
303
+ ): void {
304
+ const sysSheet = ss.getSheetByName(DDL_SYS_TABLES_SHEET);
305
+ if (!sysSheet) return;
306
+
307
+ const data = sysSheet.getDataRange().getValues();
308
+ for (let i = data.length - 1; i >= 1; i--) {
309
+ if (data[i][0] === tableName) {
310
+ sysSheet.deleteRow(i + 1);
311
+ this.logger?.info('DBDDLV2.unregisterFromSysTables', `Removed "${tableName}" from __sys__tables__`, {});
312
+ return;
313
+ }
314
+ }
315
+ }
316
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "libraries": {
3
+ "spreadsheets_db": {
4
+ "file": "spreadsheets_db.ts",
5
+ "description": "Database abstraction layer for Google Sheets",
6
+ "version": "2.0.0",
7
+ "order": 0,
8
+ "required": true,
9
+ "source": "local"
10
+ },
11
+ "gaslogger": {
12
+ "file": "gaslogger.ts",
13
+ "description": "Structured logging utility",
14
+ "version": "1.0.0",
15
+ "order": 1,
16
+ "required": false,
17
+ "dependsOn": ["gaserror"],
18
+ "source": "common"
19
+ },
20
+ "gaserror": {
21
+ "file": "gaserror.ts",
22
+ "description": "Error handling for Google Apps Script",
23
+ "version": "1.0.0",
24
+ "order": 2,
25
+ "required": false,
26
+ "dependsOn": [],
27
+ "source": "common"
28
+ },
29
+ "gas_response_helper": {
30
+ "file": "gas_response_helper.ts",
31
+ "description": "API response formatting helper",
32
+ "version": "1.0.0",
33
+ "order": 3,
34
+ "required": false,
35
+ "dependsOn": [],
36
+ "source": "common"
37
+ },
38
+ "db_ddl": {
39
+ "file": "db_ddl.ts",
40
+ "description": "Schema provisioning (DDL) for Google Sheets databases",
41
+ "version": "2.0.0",
42
+ "order": 5,
43
+ "required": true,
44
+ "source": "local"
45
+ },
46
+ "alasql": {
47
+ "file": "alasql.js",
48
+ "description": "SQL query engine (native JS)",
49
+ "version": "1.0.0",
50
+ "order": 900,
51
+ "native": true,
52
+ "required": false,
53
+ "source": "local"
54
+ }
55
+ }
56
+ }