nsa-sheets-db-builder 0.0.1-alpha.2 → 0.0.1-alpha.3
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/bin/sheets-deployer.mjs +4 -1
- package/libs/common/uuid.ts +17 -0
- package/libs/db_ddl.ts +108 -35
- package/libs/libraries.json +9 -0
- package/libs/spreadsheets_db.ts +54 -47
- package/libs/triggers.ts +1 -1
- package/package.json +1 -1
- package/scripts/build.mjs +251 -83
- package/scripts/ddl-handler.mjs +4 -4
- package/scripts/gen.mjs +223 -0
- package/scripts/init.mjs +9 -5
- package/scripts/lib/utils.mjs +131 -56
- package/scripts/login.mjs +31 -18
- package/scripts/setup.mjs +156 -312
- package/src/api/ddl.ts +127 -23
- package/src/templates/admin-api.ts.ejs +74 -0
- package/src/templates/admin-panel.html +917 -0
- package/src/templates/appsscript.json.ejs +13 -11
- package/src/templates/config.ts.ejs +2 -1
- package/src/templates/main-initial.ts.ejs +116 -0
- package/src/templates/main.ts.ejs +28 -21
- package/src/templates/rbac.ts.ejs +2 -2
- package/src/templates/router.ts.ejs +207 -0
- package/templates/blank.json +2 -2
- package/templates/blog-cms.json +26 -26
- package/templates/crm.json +16 -16
- package/templates/e-commerce.json +18 -18
- package/templates/inventory.json +14 -14
- package/templates/todo.json +69 -0
package/bin/sheets-deployer.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Commands:
|
|
11
11
|
* init Initialize a new DB project from a template
|
|
12
|
+
* gen Generate app source files (config, schema, router, main)
|
|
12
13
|
* build Build GAS output for a DB project
|
|
13
14
|
* push Build + push to Google Apps Script
|
|
14
15
|
* deploy Build + push + deploy
|
|
@@ -48,6 +49,7 @@ const scriptsDir = path.resolve(__dirname, '..', 'scripts');
|
|
|
48
49
|
// Command → script mapping
|
|
49
50
|
const COMMANDS = {
|
|
50
51
|
'init': 'init.mjs',
|
|
52
|
+
'gen': 'gen.mjs',
|
|
51
53
|
'build': 'build.mjs',
|
|
52
54
|
'push': ['build.mjs', '--push'],
|
|
53
55
|
'deploy': ['build.mjs', '--push', '--deploy'],
|
|
@@ -74,7 +76,7 @@ const command = process.argv[2];
|
|
|
74
76
|
const restArgs = process.argv.slice(3);
|
|
75
77
|
|
|
76
78
|
if (!command || command === '--help' || command === '-h') {
|
|
77
|
-
console.log('nsa-sheets-db-builder v0.0.1-alpha.
|
|
79
|
+
console.log('nsa-sheets-db-builder v0.0.1-alpha.3 — Google Sheets database provisioner\n');
|
|
78
80
|
console.log('Usage: nsa-sheets-db-builder <command> [options]\n');
|
|
79
81
|
console.log('Commands:');
|
|
80
82
|
const maxLen = Math.max(...Object.keys(COMMANDS).map(k => k.length));
|
|
@@ -145,6 +147,7 @@ try {
|
|
|
145
147
|
function getDescription(cmd) {
|
|
146
148
|
const descriptions = {
|
|
147
149
|
'init': 'Initialize a new DB project from a template',
|
|
150
|
+
'gen': 'Generate app source files (config, schema, router, main)',
|
|
148
151
|
'build': 'Build GAS output for a DB project (all instances)',
|
|
149
152
|
'push': 'Build + push instance to Google Apps Script',
|
|
150
153
|
'deploy': 'Build + push + deploy instance',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// UUID utility class for ID generation and hashing
|
|
2
|
+
class UUID {
|
|
3
|
+
static generate(input?: string): string {
|
|
4
|
+
if (input) {
|
|
5
|
+
const raw = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, input);
|
|
6
|
+
const hex = raw.map((b: number) => ('0' + ((b + 256) % 256).toString(16)).slice(-2)).join('');
|
|
7
|
+
return hex.substring(0, 8) + '-' + hex.substring(8, 12) + '-4' + hex.substring(13, 16) + '-' + hex.substring(16, 20) + '-' + hex.substring(20, 32);
|
|
8
|
+
}
|
|
9
|
+
return Utilities.getUuid();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class pdUUIDStatic {
|
|
14
|
+
static generate(input?: string): string {
|
|
15
|
+
return UUID.generate(input);
|
|
16
|
+
}
|
|
17
|
+
}
|
package/libs/db_ddl.ts
CHANGED
|
@@ -23,7 +23,7 @@ declare class GASLoggerV2 {
|
|
|
23
23
|
// ── Constants ─────────────────────────────
|
|
24
24
|
|
|
25
25
|
const DDL_SYS_TABLES_SHEET = '__sys__tables__';
|
|
26
|
-
const DDL_SYS_TABLES_HEADERS = ['
|
|
26
|
+
const DDL_SYS_TABLES_HEADERS = ['id', 'table_name', 'spreadsheet_id', 'counter', 'last_update', 'last_update_hash', '__audit', '__archived'];
|
|
27
27
|
|
|
28
28
|
// ── Types ─────────────────────────────────
|
|
29
29
|
|
|
@@ -35,7 +35,6 @@ interface DDLCreateSpreadsheetResult {
|
|
|
35
35
|
|
|
36
36
|
interface DDLTableResult {
|
|
37
37
|
name: string;
|
|
38
|
-
sheetName: string;
|
|
39
38
|
spreadsheetId: string;
|
|
40
39
|
status: 'created' | 'exists' | 'skipped';
|
|
41
40
|
columns?: number;
|
|
@@ -59,9 +58,9 @@ interface DDLDescribeResult {
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
interface DDLSyncResult {
|
|
62
|
-
created: { name: string;
|
|
63
|
-
existing: { name: string;
|
|
64
|
-
drift: { name: string;
|
|
61
|
+
created: { name: string; spreadsheetId: string; columns: number }[];
|
|
62
|
+
existing: { name: string; spreadsheetId: string }[];
|
|
63
|
+
drift: { name: string; spreadsheetId: string; missing: string[]; extra: string[] }[];
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
// ── Core DDL Class ────────────────────────
|
|
@@ -99,6 +98,59 @@ class DBDDLV2 {
|
|
|
99
98
|
* Provision tables from a DBConfigV2-shaped tables object.
|
|
100
99
|
* Creates sheets with headers + __sys__tables__ entries for each table.
|
|
101
100
|
*/
|
|
101
|
+
/**
|
|
102
|
+
* Provision a single table as its own spreadsheet inside the given folder.
|
|
103
|
+
* Spreadsheet name: project_name__table_name__env_name
|
|
104
|
+
* Sheet name inside = tableName
|
|
105
|
+
*/
|
|
106
|
+
provisionTable(
|
|
107
|
+
systemSs: GoogleAppsScript.Spreadsheet.Spreadsheet,
|
|
108
|
+
folder: GoogleAppsScript.Drive.Folder,
|
|
109
|
+
projectName: string,
|
|
110
|
+
envName: string,
|
|
111
|
+
tableName: string,
|
|
112
|
+
schema: Record<string, any>
|
|
113
|
+
): DDLTableResult {
|
|
114
|
+
const headers = Object.keys(schema);
|
|
115
|
+
|
|
116
|
+
// Spreadsheet name: project_name__table_name__env_name
|
|
117
|
+
const spreadsheetName = `${projectName}__${tableName}__${envName}`;
|
|
118
|
+
const ss = SpreadsheetApp.create(spreadsheetName);
|
|
119
|
+
const spreadsheetId = ss.getId();
|
|
120
|
+
|
|
121
|
+
// Move into the Drive folder
|
|
122
|
+
const file = DriveApp.getFileById(spreadsheetId);
|
|
123
|
+
folder.addFile(file);
|
|
124
|
+
DriveApp.getRootFolder().removeFile(file);
|
|
125
|
+
|
|
126
|
+
// Rename default Sheet1 to tableName
|
|
127
|
+
const sheet = ss.getSheets()[0];
|
|
128
|
+
sheet.setName(tableName);
|
|
129
|
+
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
|
130
|
+
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
|
131
|
+
|
|
132
|
+
// Trim extra columns (default 26 → schema column count)
|
|
133
|
+
const maxCols = sheet.getMaxColumns();
|
|
134
|
+
if (maxCols > headers.length) {
|
|
135
|
+
sheet.deleteColumns(headers.length + 1, maxCols - headers.length);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Trim extra rows (default 1000 → just the header row)
|
|
139
|
+
const maxRows = sheet.getMaxRows();
|
|
140
|
+
if (maxRows > 1) {
|
|
141
|
+
sheet.deleteRows(2, maxRows - 1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Register in __sys__tables__ on the system spreadsheet
|
|
145
|
+
this.registerInSysTables(systemSs, tableName, spreadsheetId);
|
|
146
|
+
|
|
147
|
+
this.logger?.info('DBDDLV2.provisionTable', `Created spreadsheet "${tableName}" with ${headers.length} columns`, { spreadsheetId });
|
|
148
|
+
return { name: tableName, spreadsheetId, status: 'created', columns: headers.length };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Provision tables from a DBConfigV2-shaped tables object (legacy — all in one spreadsheet).
|
|
153
|
+
*/
|
|
102
154
|
provisionTables(spreadsheetId: string, tables: Record<string, any>): DDLProvisionResult {
|
|
103
155
|
if (!spreadsheetId) throw new Error('Missing "spreadsheetId" parameter');
|
|
104
156
|
if (!tables || typeof tables !== 'object') throw new Error('Missing or invalid "tables" parameter');
|
|
@@ -109,34 +161,40 @@ class DBDDLV2 {
|
|
|
109
161
|
const results: DDLTableResult[] = [];
|
|
110
162
|
|
|
111
163
|
for (const [tableName, tableConfig] of Object.entries(tables)) {
|
|
112
|
-
const sheetName = tableConfig.sheetName || tableName;
|
|
113
164
|
const schema = tableConfig.schema;
|
|
114
165
|
|
|
115
166
|
if (!schema) {
|
|
116
167
|
this.logger?.warn('DBDDLV2.provisionTables', `Table "${tableName}" has no schema, skipping`, {});
|
|
117
|
-
results.push({ name: tableName,
|
|
168
|
+
results.push({ name: tableName, spreadsheetId, status: 'skipped', reason: 'no schema' });
|
|
118
169
|
continue;
|
|
119
170
|
}
|
|
120
171
|
|
|
121
|
-
let sheet = ss.getSheetByName(
|
|
172
|
+
let sheet = ss.getSheetByName(tableName);
|
|
122
173
|
if (sheet) {
|
|
123
|
-
this.logger?.info('DBDDLV2.provisionTables', `Sheet "${
|
|
124
|
-
results.push({ name: tableName,
|
|
174
|
+
this.logger?.info('DBDDLV2.provisionTables', `Sheet "${tableName}" already exists, skipping`, {});
|
|
175
|
+
results.push({ name: tableName, spreadsheetId, status: 'exists' });
|
|
125
176
|
continue;
|
|
126
177
|
}
|
|
127
178
|
|
|
128
|
-
|
|
129
|
-
sheet = ss.insertSheet(sheetName);
|
|
179
|
+
sheet = ss.insertSheet(tableName);
|
|
130
180
|
const headers = Object.keys(schema);
|
|
131
181
|
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
|
|
132
182
|
sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
|
|
133
|
-
|
|
183
|
+
|
|
184
|
+
const maxCols = sheet.getMaxColumns();
|
|
185
|
+
if (maxCols > headers.length) {
|
|
186
|
+
sheet.deleteColumns(headers.length + 1, maxCols - headers.length);
|
|
187
|
+
}
|
|
134
188
|
|
|
135
|
-
|
|
136
|
-
|
|
189
|
+
const maxRows = sheet.getMaxRows();
|
|
190
|
+
if (maxRows > 1) {
|
|
191
|
+
sheet.deleteRows(2, maxRows - 1);
|
|
192
|
+
}
|
|
137
193
|
|
|
138
|
-
this.
|
|
139
|
-
|
|
194
|
+
this.registerInSysTables(ss, tableName, spreadsheetId);
|
|
195
|
+
|
|
196
|
+
this.logger?.info('DBDDLV2.provisionTables', `Created sheet "${tableName}" with ${headers.length} columns`, {});
|
|
197
|
+
results.push({ name: tableName, spreadsheetId, status: 'created', columns: headers.length });
|
|
140
198
|
}
|
|
141
199
|
|
|
142
200
|
return { tables: results };
|
|
@@ -227,22 +285,28 @@ class DBDDLV2 {
|
|
|
227
285
|
const drift: DDLSyncResult['drift'] = [];
|
|
228
286
|
|
|
229
287
|
for (const [tableName, tableConfig] of Object.entries(tables)) {
|
|
230
|
-
const sheetName = tableConfig.sheetName || tableName;
|
|
231
288
|
const schema = tableConfig.schema;
|
|
232
289
|
if (!schema) continue;
|
|
233
290
|
|
|
234
291
|
const expectedHeaders = Object.keys(schema);
|
|
235
|
-
const sheet = ss.getSheetByName(
|
|
292
|
+
const sheet = ss.getSheetByName(tableName);
|
|
236
293
|
|
|
237
294
|
if (!sheet) {
|
|
238
|
-
// Create missing sheet
|
|
239
|
-
const newSheet = ss.insertSheet(
|
|
295
|
+
// Create missing sheet — trim to exact schema size
|
|
296
|
+
const newSheet = ss.insertSheet(tableName);
|
|
240
297
|
newSheet.getRange(1, 1, 1, expectedHeaders.length).setValues([expectedHeaders]);
|
|
241
298
|
newSheet.getRange(1, 1, 1, expectedHeaders.length).setFontWeight('bold');
|
|
242
|
-
newSheet.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
299
|
+
const syncMaxCols = newSheet.getMaxColumns();
|
|
300
|
+
if (syncMaxCols > expectedHeaders.length) {
|
|
301
|
+
newSheet.deleteColumns(expectedHeaders.length + 1, syncMaxCols - expectedHeaders.length);
|
|
302
|
+
}
|
|
303
|
+
const syncMaxRows = newSheet.getMaxRows();
|
|
304
|
+
if (syncMaxRows > 1) {
|
|
305
|
+
newSheet.deleteRows(2, syncMaxRows - 1);
|
|
306
|
+
}
|
|
307
|
+
this.registerInSysTables(ss, tableName, spreadsheetId);
|
|
308
|
+
|
|
309
|
+
created.push({ name: tableName, spreadsheetId, columns: expectedHeaders.length });
|
|
246
310
|
continue;
|
|
247
311
|
}
|
|
248
312
|
|
|
@@ -256,9 +320,9 @@ class DBDDLV2 {
|
|
|
256
320
|
const extra = actualHeaders.filter(h => !expectedHeaders.includes(h));
|
|
257
321
|
|
|
258
322
|
if (missing.length > 0 || extra.length > 0) {
|
|
259
|
-
drift.push({ name: tableName,
|
|
323
|
+
drift.push({ name: tableName, spreadsheetId, missing, extra });
|
|
260
324
|
} else {
|
|
261
|
-
existing.push({ name: tableName,
|
|
325
|
+
existing.push({ name: tableName, spreadsheetId });
|
|
262
326
|
}
|
|
263
327
|
}
|
|
264
328
|
|
|
@@ -267,31 +331,40 @@ class DBDDLV2 {
|
|
|
267
331
|
|
|
268
332
|
// ── Internal helpers ──────────────────────
|
|
269
333
|
|
|
270
|
-
|
|
334
|
+
ensureSysTablesSheet(ss: GoogleAppsScript.Spreadsheet.Spreadsheet): GoogleAppsScript.Spreadsheet.Sheet {
|
|
271
335
|
let sheet = ss.getSheetByName(DDL_SYS_TABLES_SHEET);
|
|
272
336
|
if (sheet) return sheet;
|
|
273
337
|
|
|
274
338
|
sheet = ss.insertSheet(DDL_SYS_TABLES_SHEET);
|
|
275
339
|
sheet.getRange(1, 1, 1, DDL_SYS_TABLES_HEADERS.length).setValues([DDL_SYS_TABLES_HEADERS]);
|
|
276
340
|
sheet.getRange(1, 1, 1, DDL_SYS_TABLES_HEADERS.length).setFontWeight('bold');
|
|
277
|
-
|
|
341
|
+
|
|
342
|
+
// Trim to exact size
|
|
343
|
+
const sysCols = sheet.getMaxColumns();
|
|
344
|
+
if (sysCols > DDL_SYS_TABLES_HEADERS.length) {
|
|
345
|
+
sheet.deleteColumns(DDL_SYS_TABLES_HEADERS.length + 1, sysCols - DDL_SYS_TABLES_HEADERS.length);
|
|
346
|
+
}
|
|
347
|
+
const sysRows = sheet.getMaxRows();
|
|
348
|
+
if (sysRows > 1) {
|
|
349
|
+
sheet.deleteRows(2, sysRows - 1);
|
|
350
|
+
}
|
|
278
351
|
|
|
279
352
|
this.logger?.info('DBDDLV2.ensureSysTablesSheet', 'Created __sys__tables__ metadata sheet', {});
|
|
280
353
|
|
|
281
354
|
return sheet;
|
|
282
355
|
}
|
|
283
356
|
|
|
284
|
-
|
|
357
|
+
registerInSysTables(
|
|
285
358
|
ss: GoogleAppsScript.Spreadsheet.Spreadsheet,
|
|
286
359
|
tableName: string,
|
|
287
|
-
|
|
288
|
-
spreadsheetId: string,
|
|
289
|
-
columnCount: number
|
|
360
|
+
spreadsheetId: string
|
|
290
361
|
): void {
|
|
291
362
|
const sysSheet = ss.getSheetByName(DDL_SYS_TABLES_SHEET);
|
|
292
363
|
if (!sysSheet) return;
|
|
293
364
|
|
|
294
|
-
const
|
|
365
|
+
const id = Utilities.getUuid();
|
|
366
|
+
// Columns: id, table_name, spreadsheet_id, counter, last_update, last_update_hash, __audit, __archived
|
|
367
|
+
const row = [id, tableName, spreadsheetId, 0, '', '', '', false];
|
|
295
368
|
sysSheet.appendRow(row);
|
|
296
369
|
|
|
297
370
|
this.logger?.info('DBDDLV2.registerInSysTables', `Registered "${tableName}" in __sys__tables__`, {});
|
|
@@ -306,7 +379,7 @@ class DBDDLV2 {
|
|
|
306
379
|
|
|
307
380
|
const data = sysSheet.getDataRange().getValues();
|
|
308
381
|
for (let i = data.length - 1; i >= 1; i--) {
|
|
309
|
-
if (data[i][
|
|
382
|
+
if (data[i][1] === tableName) { // column B = table_name
|
|
310
383
|
sysSheet.deleteRow(i + 1);
|
|
311
384
|
this.logger?.info('DBDDLV2.unregisterFromSysTables', `Removed "${tableName}" from __sys__tables__`, {});
|
|
312
385
|
return;
|
package/libs/libraries.json
CHANGED
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
"dependsOn": [],
|
|
36
36
|
"source": "common"
|
|
37
37
|
},
|
|
38
|
+
"uuid": {
|
|
39
|
+
"file": "uuid.ts",
|
|
40
|
+
"description": "UUID generation and hashing utility",
|
|
41
|
+
"version": "1.0.0",
|
|
42
|
+
"order": 3,
|
|
43
|
+
"required": true,
|
|
44
|
+
"dependsOn": [],
|
|
45
|
+
"source": "common"
|
|
46
|
+
},
|
|
38
47
|
"db_ddl": {
|
|
39
48
|
"file": "db_ddl.ts",
|
|
40
49
|
"description": "Schema provisioning (DDL) for Google Sheets databases",
|
package/libs/spreadsheets_db.ts
CHANGED
|
@@ -14,12 +14,11 @@ const SYSTEM_TABLES: { [tableName: string]: Partial<TableConfig> } = {
|
|
|
14
14
|
id: { type: 'string', primaryKey: true, required: true },
|
|
15
15
|
table_name: { type: 'string', required: true },
|
|
16
16
|
spreadsheet_id: { type: 'string', required: true },
|
|
17
|
-
sheet_name: { type: 'string', required: true },
|
|
18
17
|
counter: { type: 'number', defaultValue: 0 },
|
|
19
18
|
last_update: { type: 'datetime' },
|
|
20
19
|
last_update_hash: { type: 'string' },
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
__audit: { type: 'object' },
|
|
21
|
+
__archived: { type: 'boolean', defaultValue: false }
|
|
23
22
|
},
|
|
24
23
|
idGenerator: 'STATIC_UUID',
|
|
25
24
|
readOnly: false,
|
|
@@ -140,7 +139,6 @@ interface CachedUser {
|
|
|
140
139
|
interface TableRuntimeState {
|
|
141
140
|
tableName: string;
|
|
142
141
|
spreadsheetId: string;
|
|
143
|
-
sheetName: string;
|
|
144
142
|
counter: number;
|
|
145
143
|
lastUpdate: number; // Unix timestamp in milliseconds
|
|
146
144
|
lastUpdateHash: string;
|
|
@@ -149,17 +147,10 @@ interface TableRuntimeState {
|
|
|
149
147
|
interface TableMetadataRow {
|
|
150
148
|
table_name: string;
|
|
151
149
|
spreadsheet_id: string;
|
|
152
|
-
sheet_name: string;
|
|
153
|
-
schema: string; // JSON stringified
|
|
154
150
|
counter: number;
|
|
155
151
|
last_update: string;
|
|
156
152
|
last_update_hash: string;
|
|
157
|
-
|
|
158
|
-
read_only: boolean;
|
|
159
|
-
delete_mode: string;
|
|
160
|
-
created_at: string;
|
|
161
|
-
updated_at: string;
|
|
162
|
-
__archived__: boolean;
|
|
153
|
+
__archived: boolean;
|
|
163
154
|
}
|
|
164
155
|
|
|
165
156
|
interface EntityLock {
|
|
@@ -232,8 +223,8 @@ class TableV2 {
|
|
|
232
223
|
db: DBV2 | null = null
|
|
233
224
|
) {
|
|
234
225
|
this.tableName = tableName;
|
|
235
|
-
this.spreadsheetId = tableConfig.spreadsheetId;
|
|
236
|
-
this.sheetName = tableConfig.sheetName;
|
|
226
|
+
this.spreadsheetId = runtimeState?.spreadsheetId || tableConfig.spreadsheetId;
|
|
227
|
+
this.sheetName = tableConfig.sheetName || tableName;
|
|
237
228
|
this.schema = tableConfig.schema;
|
|
238
229
|
this.runtimeState = runtimeState;
|
|
239
230
|
this.metadataSpreadsheetId = metadataSpreadsheetId;
|
|
@@ -263,7 +254,7 @@ class TableV2 {
|
|
|
263
254
|
}
|
|
264
255
|
|
|
265
256
|
private hasAuditField(): boolean {
|
|
266
|
-
return '
|
|
257
|
+
return '__audit' in this.schema;
|
|
267
258
|
}
|
|
268
259
|
|
|
269
260
|
private createAuditData(operation: 'create' | 'update', oldData?: any, newData?: any): AuditData {
|
|
@@ -361,7 +352,7 @@ class TableV2 {
|
|
|
361
352
|
private getLastRow(): number {
|
|
362
353
|
try {
|
|
363
354
|
const sheet = this.getSheet();
|
|
364
|
-
return sheet.
|
|
355
|
+
return sheet.getLastRow();
|
|
365
356
|
} catch (error) {
|
|
366
357
|
GASErrorV2.handleError(
|
|
367
358
|
error,
|
|
@@ -536,6 +527,14 @@ class TableV2 {
|
|
|
536
527
|
}
|
|
537
528
|
}
|
|
538
529
|
|
|
530
|
+
list(options: any = {}): any {
|
|
531
|
+
return this.getData(options.pageStart || 0, options.pageSize || 0);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
get(id: string): any {
|
|
535
|
+
return this.getById(id);
|
|
536
|
+
}
|
|
537
|
+
|
|
539
538
|
getById(id: string): any {
|
|
540
539
|
try {
|
|
541
540
|
this.initializeTableDetails();
|
|
@@ -676,7 +675,7 @@ class TableV2 {
|
|
|
676
675
|
for (const [columnName, columnDef] of Object.entries(this.schema)) {
|
|
677
676
|
if (columnName === this.primaryKey) {
|
|
678
677
|
newRow[index] = typedId.toString();
|
|
679
|
-
} else if (columnName === '
|
|
678
|
+
} else if (columnName === '__audit') {
|
|
680
679
|
// Inject audit data
|
|
681
680
|
newRow[index] = auditData ? JSON.stringify(auditData) : '';
|
|
682
681
|
} else if (entry.hasOwnProperty(columnName)) {
|
|
@@ -695,7 +694,7 @@ class TableV2 {
|
|
|
695
694
|
newRow[index] = formatDateTime();
|
|
696
695
|
} else if (columnName === '__last_updated_by__') {
|
|
697
696
|
newRow[index] = Session.getActiveUser().getEmail();
|
|
698
|
-
} else if (columnName === '
|
|
697
|
+
} else if (columnName === '__archived') {
|
|
699
698
|
newRow[index] = entry[columnName] !== undefined ? entry[columnName] : false;
|
|
700
699
|
} else if (columnDef.defaultValue !== undefined) {
|
|
701
700
|
newRow[index] = columnDef.defaultValue;
|
|
@@ -803,7 +802,7 @@ class TableV2 {
|
|
|
803
802
|
const hasAudit = this.hasAuditField();
|
|
804
803
|
let existingAuditData: AuditData | null = null;
|
|
805
804
|
if (hasAudit) {
|
|
806
|
-
const auditIndex = Object.keys(this.schema).indexOf('
|
|
805
|
+
const auditIndex = Object.keys(this.schema).indexOf('__audit');
|
|
807
806
|
try {
|
|
808
807
|
existingAuditData = JSON.parse(rowData[auditIndex]) as AuditData;
|
|
809
808
|
} catch {
|
|
@@ -814,7 +813,7 @@ class TableV2 {
|
|
|
814
813
|
// Update the row with new data
|
|
815
814
|
let index = 0;
|
|
816
815
|
for (const [columnName, columnDef] of Object.entries(this.schema)) {
|
|
817
|
-
if (columnName === '
|
|
816
|
+
if (columnName === '__audit') {
|
|
818
817
|
// Update audit data
|
|
819
818
|
const newAuditData = this.createAuditData('update', existingAuditData, data);
|
|
820
819
|
rowData[index] = JSON.stringify(newAuditData);
|
|
@@ -932,9 +931,9 @@ class TableV2 {
|
|
|
932
931
|
|
|
933
932
|
private softDelete(id: any): any {
|
|
934
933
|
try {
|
|
935
|
-
if (!this.schema.hasOwnProperty('
|
|
934
|
+
if (!this.schema.hasOwnProperty('__archived')) {
|
|
936
935
|
throw new GASErrorV2(
|
|
937
|
-
`Soft delete not possible, "
|
|
936
|
+
`Soft delete not possible, "__archived" field not found in schema`,
|
|
938
937
|
'TableV2.softDelete',
|
|
939
938
|
{}, this.logger?.getTraceId()
|
|
940
939
|
);
|
|
@@ -946,7 +945,7 @@ class TableV2 {
|
|
|
946
945
|
}
|
|
947
946
|
|
|
948
947
|
const sheet = this.getSheet();
|
|
949
|
-
const archivedColumnIndex = Object.keys(this.schema).indexOf('
|
|
948
|
+
const archivedColumnIndex = Object.keys(this.schema).indexOf('__archived');
|
|
950
949
|
sheet.getRange(rowIndex, archivedColumnIndex + 1).setValue(true);
|
|
951
950
|
this.updateLastTableUpdateInfo(formatDateTime(), { id, method: 'softDelete' });
|
|
952
951
|
this.logger?.info('TableV2.softDelete', `Soft deleted row with ID '${id}' in table ${this.tableName}`, {});
|
|
@@ -1015,12 +1014,12 @@ class TableV2 {
|
|
|
1015
1014
|
throw new Error(`Table '${this.tableName}' not found in __sys__tables__`);
|
|
1016
1015
|
}
|
|
1017
1016
|
|
|
1018
|
-
// Column
|
|
1019
|
-
const currentCounter = sysTablesSheet.getRange(rowIndex,
|
|
1017
|
+
// Column 4 is counter (id=1, table_name=2, spreadsheet_id=3, counter=4)
|
|
1018
|
+
const currentCounter = sysTablesSheet.getRange(rowIndex, 4).getValue();
|
|
1020
1019
|
this.logger?.debug('TableV2.updateCounter', 'Read current counter value', { tableName: this.tableName, currentCounter, rowIndex });
|
|
1021
1020
|
const newCounter = Number(currentCounter) + 1;
|
|
1022
1021
|
this.logger?.debug('TableV2.updateCounter', 'Calculated new counter', { tableName: this.tableName, newCounter });
|
|
1023
|
-
sysTablesSheet.getRange(rowIndex,
|
|
1022
|
+
sysTablesSheet.getRange(rowIndex, 4).setValue(newCounter);
|
|
1024
1023
|
|
|
1025
1024
|
// Update in-memory counter
|
|
1026
1025
|
this.counter = newCounter;
|
|
@@ -1114,8 +1113,8 @@ class TableV2 {
|
|
|
1114
1113
|
}
|
|
1115
1114
|
|
|
1116
1115
|
// Debug logging for boolean conversion
|
|
1117
|
-
if (columnName === '
|
|
1118
|
-
this.logger?.debug('TableV2.toArrayOfObjects', `Boolean conversion for
|
|
1116
|
+
if (columnName === '__archived' && this.tableName === 'epuap_tickets') {
|
|
1117
|
+
this.logger?.debug('TableV2.toArrayOfObjects', `Boolean conversion for __archived`, {
|
|
1119
1118
|
originalValue: row[index],
|
|
1120
1119
|
originalType: typeof row[index],
|
|
1121
1120
|
convertedValue: value,
|
|
@@ -1607,6 +1606,18 @@ class DBV2 {
|
|
|
1607
1606
|
this.initializeSystemCaches();
|
|
1608
1607
|
}
|
|
1609
1608
|
|
|
1609
|
+
/**
|
|
1610
|
+
* Get a table instance by name. Initializes lazily from __sys__tables__ runtime state.
|
|
1611
|
+
*/
|
|
1612
|
+
table(tableName: string): TableV2 | null {
|
|
1613
|
+
try {
|
|
1614
|
+
this.initializeTable(tableName);
|
|
1615
|
+
return this.tables.get(tableName) || null;
|
|
1616
|
+
} catch {
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1610
1621
|
// ========================================
|
|
1611
1622
|
// Two-Level Locking Mechanism
|
|
1612
1623
|
// ========================================
|
|
@@ -1943,8 +1954,7 @@ class DBV2 {
|
|
|
1943
1954
|
if (dataRecords.length === 0) {
|
|
1944
1955
|
this.logger?.info('DBV2.cacheSystemTable', `Cached empty table '${tableName}' (0 records) - table exists but has no data`, {
|
|
1945
1956
|
cacheKey: cacheKey,
|
|
1946
|
-
spreadsheetId: runtimeState.spreadsheetId
|
|
1947
|
-
sheetName: runtimeState.sheetName
|
|
1957
|
+
spreadsheetId: runtimeState.spreadsheetId
|
|
1948
1958
|
});
|
|
1949
1959
|
} else {
|
|
1950
1960
|
this.logger?.info('DBV2.cacheSystemTable', `Cached ${dataRecords.length} records from '${tableName}'`, {
|
|
@@ -1991,7 +2001,7 @@ class DBV2 {
|
|
|
1991
2001
|
|
|
1992
2002
|
// Build mapping: role -> array of user emails
|
|
1993
2003
|
for (const user of users) {
|
|
1994
|
-
if (user.
|
|
2004
|
+
if (user.__archived === false && user.active !== false && user.email) {
|
|
1995
2005
|
const role = user.role;
|
|
1996
2006
|
if (!roleUsers[role]) {
|
|
1997
2007
|
roleUsers[role] = [];
|
|
@@ -2406,12 +2416,12 @@ class DBV2 {
|
|
|
2406
2416
|
return;
|
|
2407
2417
|
}
|
|
2408
2418
|
|
|
2409
|
-
// Read runtime state
|
|
2410
|
-
const data = metadataSheet.getRange(2, 1, lastRow - 1,
|
|
2419
|
+
// Read runtime state (8 columns: id, table_name, spreadsheet_id, counter, last_update, last_update_hash, __audit, __archived)
|
|
2420
|
+
const data = metadataSheet.getRange(2, 1, lastRow - 1, 8).getValues();
|
|
2411
2421
|
const runtimeStates: TableRuntimeState[] = [];
|
|
2412
2422
|
|
|
2413
2423
|
for (const row of data) {
|
|
2414
|
-
const archived = row[
|
|
2424
|
+
const archived = row[7]; // __archived column (8th column, index 7)
|
|
2415
2425
|
if (archived) {
|
|
2416
2426
|
continue; // Skip archived tables
|
|
2417
2427
|
}
|
|
@@ -2419,10 +2429,9 @@ class DBV2 {
|
|
|
2419
2429
|
const state: TableRuntimeState = {
|
|
2420
2430
|
tableName: row[1], // table_name
|
|
2421
2431
|
spreadsheetId: row[2], // spreadsheet_id
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
lastUpdateHash: row[6] // last_update_hash
|
|
2432
|
+
counter: row[3], // counter
|
|
2433
|
+
lastUpdate: row[4], // last_update
|
|
2434
|
+
lastUpdateHash: row[5] // last_update_hash
|
|
2426
2435
|
};
|
|
2427
2436
|
|
|
2428
2437
|
runtimeStates.push(state);
|
|
@@ -2538,7 +2547,7 @@ class DBV2 {
|
|
|
2538
2547
|
}
|
|
2539
2548
|
|
|
2540
2549
|
const now = new Date().getTime(); // Unix timestamp in milliseconds
|
|
2541
|
-
const currentCounter = metadataSheet.getRange(rowIndex,
|
|
2550
|
+
const currentCounter = metadataSheet.getRange(rowIndex, 4).getValue(); // Column D: counter (read only, don't modify)
|
|
2542
2551
|
|
|
2543
2552
|
// Calculate new hash
|
|
2544
2553
|
const runtimeState = this.getRuntimeState(tableName);
|
|
@@ -2547,15 +2556,14 @@ class DBV2 {
|
|
|
2547
2556
|
JSON.stringify({
|
|
2548
2557
|
tableName: tableName,
|
|
2549
2558
|
spreadsheetId: runtimeState?.spreadsheetId,
|
|
2550
|
-
|
|
2551
|
-
counter: currentCounter, // Use current counter value, don't increment
|
|
2559
|
+
counter: currentCounter,
|
|
2552
2560
|
last_update: now
|
|
2553
2561
|
})
|
|
2554
2562
|
).map(byte => (byte < 0 ? byte + 256 : byte).toString(16).padStart(2, '0')).join('');
|
|
2555
2563
|
|
|
2556
2564
|
// Update: last_update, last_update_hash (counter updated separately by TableV2.updateCounter)
|
|
2557
|
-
metadataSheet.getRange(rowIndex,
|
|
2558
|
-
metadataSheet.getRange(rowIndex,
|
|
2565
|
+
metadataSheet.getRange(rowIndex, 5).setValue(now); // Column E: last_update (Unix timestamp)
|
|
2566
|
+
metadataSheet.getRange(rowIndex, 6).setValue(newHash); // Column F: last_update_hash
|
|
2559
2567
|
|
|
2560
2568
|
this.logger?.debug('DBV2.updateRuntimeState', `Updated runtime state for ${tableName}`, {
|
|
2561
2569
|
rowIndex,
|
|
@@ -2612,9 +2620,8 @@ class DBV2 {
|
|
|
2612
2620
|
const configTable = this.config.tables[state.tableName];
|
|
2613
2621
|
|
|
2614
2622
|
if (configTable) {
|
|
2615
|
-
// Table exists in config - update with runtime
|
|
2623
|
+
// Table exists in config - update with runtime spreadsheetId from __sys__tables__
|
|
2616
2624
|
configTable.spreadsheetId = state.spreadsheetId;
|
|
2617
|
-
configTable.sheetName = state.sheetName;
|
|
2618
2625
|
this.logger?.debug('DBV2.loadTablesFromMetadata', `Merged runtime state for table: ${state.tableName}`);
|
|
2619
2626
|
} else {
|
|
2620
2627
|
// Table exists in runtime but not in config - warn but don't create
|
|
@@ -2831,8 +2838,8 @@ class DBV2 {
|
|
|
2831
2838
|
return filtered as T;
|
|
2832
2839
|
}
|
|
2833
2840
|
|
|
2834
|
-
// Fallback: exclude
|
|
2835
|
-
const defaultExclude = ['
|
|
2841
|
+
// Fallback: exclude __audit and __archived for non-superadmin if no readFields specified
|
|
2842
|
+
const defaultExclude = ['__audit', '__archived'];
|
|
2836
2843
|
for (const key in record) {
|
|
2837
2844
|
if (!defaultExclude.includes(key)) {
|
|
2838
2845
|
filtered[key] = record[key];
|
package/libs/triggers.ts
CHANGED
|
@@ -80,7 +80,7 @@ function rebuildSearchIndex(): void {
|
|
|
80
80
|
|
|
81
81
|
if (textFields.length === 0) continue;
|
|
82
82
|
|
|
83
|
-
const result = table.list({ filters: {
|
|
83
|
+
const result = table.list({ filters: { __archived: false } });
|
|
84
84
|
const rows = result?.data || [];
|
|
85
85
|
|
|
86
86
|
for (const row of rows) {
|