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.
@@ -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.2 — Google Sheets database provisioner\n');
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 = ['table_name', 'sheet_name', 'spreadsheet_id', 'created_at', 'column_count'];
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; sheetName: string; columns: number }[];
63
- existing: { name: string; sheetName: string }[];
64
- drift: { name: string; sheetName: string; missing: string[]; extra: 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, sheetName, spreadsheetId, status: 'skipped', reason: 'no schema' });
168
+ results.push({ name: tableName, spreadsheetId, status: 'skipped', reason: 'no schema' });
118
169
  continue;
119
170
  }
120
171
 
121
- let sheet = ss.getSheetByName(sheetName);
172
+ let sheet = ss.getSheetByName(tableName);
122
173
  if (sheet) {
123
- this.logger?.info('DBDDLV2.provisionTables', `Sheet "${sheetName}" already exists, skipping`, {});
124
- results.push({ name: tableName, sheetName, spreadsheetId, status: 'exists' });
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
- // Create sheet with headers
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
- sheet.setFrozenRows(1);
183
+
184
+ const maxCols = sheet.getMaxColumns();
185
+ if (maxCols > headers.length) {
186
+ sheet.deleteColumns(headers.length + 1, maxCols - headers.length);
187
+ }
134
188
 
135
- // Register in __sys__tables__
136
- this.registerInSysTables(ss, tableName, sheetName, spreadsheetId, headers.length);
189
+ const maxRows = sheet.getMaxRows();
190
+ if (maxRows > 1) {
191
+ sheet.deleteRows(2, maxRows - 1);
192
+ }
137
193
 
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 });
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(sheetName);
292
+ const sheet = ss.getSheetByName(tableName);
236
293
 
237
294
  if (!sheet) {
238
- // Create missing sheet
239
- const newSheet = ss.insertSheet(sheetName);
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.setFrozenRows(1);
243
- this.registerInSysTables(ss, tableName, sheetName, spreadsheetId, expectedHeaders.length);
244
-
245
- created.push({ name: tableName, sheetName, columns: expectedHeaders.length });
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, sheetName, missing, extra });
323
+ drift.push({ name: tableName, spreadsheetId, missing, extra });
260
324
  } else {
261
- existing.push({ name: tableName, sheetName });
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
- private ensureSysTablesSheet(ss: GoogleAppsScript.Spreadsheet.Spreadsheet): GoogleAppsScript.Spreadsheet.Sheet {
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
- sheet.setFrozenRows(1);
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
- private registerInSysTables(
357
+ registerInSysTables(
285
358
  ss: GoogleAppsScript.Spreadsheet.Spreadsheet,
286
359
  tableName: string,
287
- sheetName: string,
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 row = [tableName, sheetName, spreadsheetId, new Date().toISOString(), columnCount];
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][0] === tableName) {
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;
@@ -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",
@@ -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
- __audit__: { type: 'object' },
22
- __archived__: { type: 'boolean', defaultValue: false }
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
- id_generator: string;
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 '__audit__' in this.schema;
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.getMaxRows();
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 === '__audit__') {
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 === '__archived__') {
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('__audit__');
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 === '__audit__') {
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('__archived__')) {
934
+ if (!this.schema.hasOwnProperty('__archived')) {
936
935
  throw new GASErrorV2(
937
- `Soft delete not possible, "__archived__" field not found in schema`,
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('__archived__');
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 5 is counter, read-increment-write atomically
1019
- const currentCounter = sysTablesSheet.getRange(rowIndex, 5).getValue();
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, 5).setValue(newCounter);
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 === '__archived__' && this.tableName === 'epuap_tickets') {
1118
- this.logger?.debug('TableV2.toArrayOfObjects', `Boolean conversion for __archived__`, {
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.__archived__ === false && user.active !== false && user.email) {
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 only (9 columns: id, table_name, spreadsheet_id, sheet_name, counter, last_update, last_update_hash, __audit__, __archived__)
2410
- const data = metadataSheet.getRange(2, 1, lastRow - 1, 9).getValues();
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[8]; // __archived__ column (9th column, index 8)
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
- sheetName: row[3], // sheet_name
2423
- counter: row[4], // counter
2424
- lastUpdate: row[5], // last_update
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, 5).getValue(); // Column E: counter (read only, don't modify)
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
- sheetName: runtimeState?.sheetName,
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, 6).setValue(now); // Column F: last_update (Unix timestamp)
2558
- metadataSheet.getRange(rowIndex, 7).setValue(newHash); // Column G: last_update_hash
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 location if needed
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 __audit__ and __archived__ for non-superadmin if no readFields specified
2835
- const defaultExclude = ['__audit__', '__archived__'];
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: { __archived__: false } });
83
+ const result = table.list({ filters: { __archived: false } });
84
84
  const rows = result?.data || [];
85
85
 
86
86
  for (const row of rows) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsa-sheets-db-builder",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.3",
4
4
  "description": "DDL provisioner for Google Sheets — creates spreadsheets, sheets, and headers",
5
5
  "type": "module",
6
6
  "license": "MIT",