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
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Clean built output for nsa-sheets-db-builder.
5
+ *
6
+ * Usage:
7
+ * node scripts/clean.mjs --db <name> # Clean dist/<name>/
8
+ * node scripts/clean.mjs # Clean all of dist/
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import { parseArgs, getDistDir, DIST_DIR } from './lib/utils.mjs';
13
+
14
+ const args = parseArgs();
15
+
16
+ if (args.db) {
17
+ const distDir = getDistDir(args.db);
18
+ if (fs.existsSync(distDir)) {
19
+ fs.rmSync(distDir, { recursive: true });
20
+ console.log(`Cleaned dist/${args.db}/`);
21
+ } else {
22
+ console.log(`dist/${args.db}/ does not exist, nothing to clean.`);
23
+ }
24
+ } else {
25
+ if (fs.existsSync(DIST_DIR)) {
26
+ fs.rmSync(DIST_DIR, { recursive: true });
27
+ console.log('Cleaned dist/ (all DBs)');
28
+ } else {
29
+ console.log('dist/ does not exist, nothing to clean.');
30
+ }
31
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Set up a new DB project: Drive folder + system spreadsheet + data spreadsheet + trigger.
5
+ * After creation, updates tables.json to replace __new__ spreadsheetIds with real IDs.
6
+ *
7
+ * Usage:
8
+ * node scripts/create.mjs --db <name> [--env <env>] [--instance <type>]
9
+ */
10
+
11
+ import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, loadTables, saveTables, saveProjectConfig, requireInstance, ensureAuthenticated } from './lib/utils.mjs';
12
+
13
+ const args = parseArgs();
14
+ const dbName = requireDb(args, 'create.mjs');
15
+ const config = loadDbConfig(dbName);
16
+ const { env, envConfig } = getEnvConfig(config, args.env);
17
+ const instance = requireInstance(args, envConfig);
18
+ const distDir = getDistDirForInstance(dbName, instance.type);
19
+
20
+ if (envConfig.systemSpreadsheetId) {
21
+ console.log(`Environment "${env}" already set up:`);
22
+ console.log(` systemSpreadsheetId: ${envConfig.systemSpreadsheetId}`);
23
+ console.log(` driveFolderId: ${envConfig.driveFolderId}`);
24
+ console.log('\nTo recreate, clear systemSpreadsheetId in project.json first.');
25
+ process.exit(0);
26
+ }
27
+
28
+ // Auth enforcement
29
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
30
+
31
+ // 1. Setup: create Drive folder + system spreadsheet + trigger
32
+ const folderName = `${config.name}-${env}`;
33
+ console.log(`Setting up ${dbName} (${env})...`);
34
+ console.log(` Creating Drive folder: ${folderName}`);
35
+
36
+ const setupResult = claspRun(distDir, 'ddlSetup', [{
37
+ name: folderName,
38
+ driveFolderId: envConfig.driveFolderId || undefined
39
+ }], { userProfile });
40
+
41
+ if (!setupResult?.systemSpreadsheetId) {
42
+ console.error('\nSetup failed:', JSON.stringify(setupResult, null, 2));
43
+ process.exit(1);
44
+ }
45
+
46
+ envConfig.driveFolderId = setupResult.folderId;
47
+ envConfig.systemSpreadsheetId = setupResult.systemSpreadsheetId;
48
+
49
+ console.log(` Drive folder: ${setupResult.folderUrl}`);
50
+ console.log(` System spreadsheet: ${setupResult.systemSpreadsheetId}`);
51
+
52
+ // 2. Check tables.json for __new__ spreadsheetIds and create data spreadsheets
53
+ const tables = loadTables(dbName);
54
+ let createdCount = 0;
55
+
56
+ // Find tables that need a new spreadsheet
57
+ const needsNew = Object.entries(tables).filter(([, t]) => t.spreadsheetId === '__new__');
58
+
59
+ if (needsNew.length > 0) {
60
+ const dataName = `${config.name}-data`;
61
+ console.log(` Creating data spreadsheet: ${dataName}`);
62
+
63
+ const dataResult = claspRun(distDir, 'ddlCreateDataSpreadsheet', [{
64
+ name: dataName,
65
+ folderId: setupResult.folderId
66
+ }], { userProfile });
67
+
68
+ if (dataResult?.spreadsheetId) {
69
+ console.log(` Data spreadsheet: ${dataResult.spreadsheetId}`);
70
+
71
+ // Replace __new__ with real spreadsheet ID in tables.json
72
+ for (const [name, table] of Object.entries(tables)) {
73
+ if (table.spreadsheetId === '__new__') {
74
+ table.spreadsheetId = dataResult.spreadsheetId;
75
+ }
76
+ }
77
+ saveTables(dbName, tables);
78
+ createdCount++;
79
+
80
+ console.log(` Updated tables.json: replaced __new__ → ${dataResult.spreadsheetId}`);
81
+ } else {
82
+ console.warn(' Warning: data spreadsheet creation result:', JSON.stringify(dataResult, null, 2));
83
+ }
84
+ }
85
+
86
+ // 3. Save project config
87
+ saveProjectConfig(dbName, config);
88
+
89
+ console.log(`\nSetup complete for ${dbName} (${env}).`);
90
+ console.log(` Written to dbs/${dbName}/project.json`);
91
+ if (createdCount > 0) {
92
+ console.log(` Updated dbs/${dbName}/tables.json with spreadsheet IDs`);
93
+ }
94
+ console.log(`\nNext: nsa-sheets-db-builder provision --db ${dbName}`);
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Standalone DDL handler management.
5
+ *
6
+ * The DDL handler is a separate, generic Apps Script project deployed
7
+ * once per Google account. It handles schema operations (create folders,
8
+ * spreadsheets, provision tables, etc.) via HTTP POST.
9
+ *
10
+ * This script can be used independently of setup.mjs to:
11
+ * - Build the DDL handler
12
+ * - Push it to Apps Script
13
+ * - Deploy it as a web app
14
+ *
15
+ * The DDL handler reference is stored in .nsaproject.json (root config)
16
+ * under environments.<env>.ddlHandler.
17
+ *
18
+ * Usage:
19
+ * node scripts/ddl-handler.mjs [--build] [--push] [--deploy]
20
+ * --db <name> DB project (for auth profile resolution)
21
+ * --env <env> Environment (default: dev)
22
+ * --force Skip account validation
23
+ * --inherit Use default clasp credentials
24
+ *
25
+ * Examples:
26
+ * # Build + push + deploy (default: does all three):
27
+ * nsa-sheets-db-builder ddl-handler --db my-app
28
+ *
29
+ * # Just rebuild (e.g. after updating libs):
30
+ * nsa-sheets-db-builder ddl-handler --db my-app --build
31
+ *
32
+ * # Push + update existing deployment:
33
+ * nsa-sheets-db-builder ddl-handler --db my-app --push --deploy
34
+ */
35
+
36
+ import fs from 'fs';
37
+ import os from 'os';
38
+ import path from 'path';
39
+ import crypto from 'crypto';
40
+ import { execSync } from 'child_process';
41
+ import {
42
+ parseArgs, requireDb, loadDbConfig, getEnvConfig,
43
+ ensureAuthenticated, resolveAccount,
44
+ loadRootConfig, saveRootConfig,
45
+ getDdlHandlerDistDir
46
+ } from './lib/utils.mjs';
47
+ import { buildDdlHandler } from './build.mjs';
48
+
49
+ // ────────────────────────────────────────
50
+ // Helpers
51
+ // ────────────────────────────────────────
52
+
53
+ function parseDeploymentId(output) {
54
+ const match = output.match(/(?:^Deployed |^- )(\S+) @/m);
55
+ return match ? match[1] : null;
56
+ }
57
+
58
+ function userFlag(profile) {
59
+ return profile ? ` --user ${profile}` : '';
60
+ }
61
+
62
+ function generateAdminKey() {
63
+ return crypto.randomBytes(24).toString('hex');
64
+ }
65
+
66
+ // ────────────────────────────────────────
67
+ // Main
68
+ // ────────────────────────────────────────
69
+
70
+ async function main() {
71
+ const args = parseArgs();
72
+ const dbName = requireDb(args, 'ddl-handler.mjs');
73
+
74
+ const config = loadDbConfig(dbName);
75
+ const { env, envConfig } = getEnvConfig(config, args.env);
76
+
77
+ const account = resolveAccount(envConfig, env);
78
+ if (!account) {
79
+ console.error('ERROR: account not set.');
80
+ console.error(`Set "account" in .nsaproject.json → environments.${env}`);
81
+ process.exit(1);
82
+ }
83
+
84
+ console.log(`\nDDL Handler — account: ${account} (via ${dbName}/${env})`);
85
+ console.log('='.repeat(50));
86
+
87
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
88
+
89
+ // Default: if no flags provided, do all three
90
+ const doBuild = args.build || (!args.build && !args.push && !args.deploy);
91
+ const doPush = args.push || (!args.build && !args.push && !args.deploy);
92
+ const doDeploy = args.deploy || (!args.build && !args.push && !args.deploy);
93
+
94
+ // Load existing handler state from root config
95
+ let rootConfig = loadRootConfig();
96
+ let rootEnv = rootConfig.environments?.[env] || {};
97
+ let handler = rootEnv.ddlHandler || {};
98
+
99
+ const ddlAdminKey = handler.ddlAdminKey || generateAdminKey();
100
+ let scriptId = handler.scriptId || '';
101
+ let deploymentId = handler.deploymentId || '';
102
+
103
+ // ── Create GAS project if needed ──
104
+
105
+ if (!scriptId) {
106
+ const title = `ddl-handler-${account.split('@')[0]}`;
107
+ console.log(`\nCreating GAS project: ${title}`);
108
+
109
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nsa-sheets-db-builder-ddl-'));
110
+ try {
111
+ execSync(
112
+ `npx --prefer-offline @google/clasp create --type webapp --title "${title}" --rootDir .${userFlag(userProfile)}`,
113
+ { cwd: tmpDir, stdio: 'pipe' }
114
+ );
115
+
116
+ const claspJson = JSON.parse(fs.readFileSync(path.join(tmpDir, '.clasp.json'), 'utf8'));
117
+ scriptId = claspJson.scriptId;
118
+
119
+ if (!scriptId) {
120
+ console.error('ERROR: clasp create succeeded but no scriptId found.');
121
+ process.exit(1);
122
+ }
123
+
124
+ console.log(` scriptId: ${scriptId}`);
125
+ } catch (err) {
126
+ const stderr = err.stderr?.toString() || '';
127
+ const stdout = err.stdout?.toString() || '';
128
+ console.error('\nclasp create failed for DDL handler:');
129
+ if (stderr) console.error(stderr);
130
+ if (stdout) console.error(stdout);
131
+ process.exit(1);
132
+ } finally {
133
+ fs.rmSync(tmpDir, { recursive: true, force: true });
134
+ }
135
+ } else {
136
+ console.log(`\nUsing existing scriptId: ${scriptId}`);
137
+ }
138
+
139
+ // ── Build ──
140
+
141
+ if (doBuild) {
142
+ console.log('\n-- Build --');
143
+ const loggingVerbosity = config.settings?.loggingVerbosity ?? 2;
144
+ const projectId = rootEnv.projectId || '';
145
+ buildDdlHandler(ddlAdminKey, loggingVerbosity, scriptId, projectId);
146
+ }
147
+
148
+ // ── Push ──
149
+
150
+ if (doPush) {
151
+ console.log('\n-- Push --');
152
+ const ddlDistDir = getDdlHandlerDistDir();
153
+
154
+ if (!fs.existsSync(path.join(ddlDistDir, '.clasp.json'))) {
155
+ console.error('ERROR: DDL handler not built yet. Run with --build first.');
156
+ process.exit(1);
157
+ }
158
+
159
+ console.log(' Pushing DDL handler...');
160
+ try {
161
+ execSync(`npx --prefer-offline @google/clasp push --force${userFlag(userProfile)}`, {
162
+ cwd: ddlDistDir,
163
+ stdio: 'inherit'
164
+ });
165
+ } catch {
166
+ console.error(' clasp push failed for DDL handler.');
167
+ process.exit(1);
168
+ }
169
+ }
170
+
171
+ // ── Deploy ──
172
+
173
+ if (doDeploy) {
174
+ console.log('\n-- Deploy --');
175
+ const ddlDistDir = getDdlHandlerDistDir();
176
+
177
+ if (deploymentId) {
178
+ console.log(` Updating deployment: ${deploymentId}`);
179
+ execSync(
180
+ `npx --prefer-offline @google/clasp update-deployment ${deploymentId} --description 'DDL Handler'${userFlag(userProfile)}`,
181
+ { cwd: ddlDistDir, stdio: 'inherit' }
182
+ );
183
+ } else {
184
+ console.log(' Creating new deployment...');
185
+ const output = execSync(
186
+ `npx --prefer-offline @google/clasp deploy --description 'DDL Handler'${userFlag(userProfile)}`,
187
+ { cwd: ddlDistDir, encoding: 'utf8' }
188
+ );
189
+ deploymentId = parseDeploymentId(output);
190
+ if (!deploymentId) {
191
+ console.error(' ERROR: could not parse deployment ID from clasp output:');
192
+ console.error(output);
193
+ process.exit(1);
194
+ }
195
+ console.log(` deploymentId: ${deploymentId}`);
196
+ }
197
+ }
198
+
199
+ // ── Save state to .nsaproject.json ──
200
+
201
+ rootConfig = loadRootConfig();
202
+ if (!rootConfig.environments) rootConfig.environments = {};
203
+ if (!rootConfig.environments[env]) rootConfig.environments[env] = {};
204
+ rootConfig.environments[env].ddlHandler = {
205
+ scriptId,
206
+ deploymentId,
207
+ ddlAdminKey
208
+ };
209
+ saveRootConfig(rootConfig);
210
+
211
+ // ── Summary ──
212
+
213
+ console.log('\n-- DDL Handler --');
214
+ console.log(` Account: ${account}`);
215
+ console.log(` Script: https://script.google.com/d/${scriptId}/edit`);
216
+ if (deploymentId) {
217
+ console.log(` Web app: https://script.google.com/macros/s/${deploymentId}/exec`);
218
+ }
219
+ console.log(` Admin key: ${ddlAdminKey.substring(0, 8)}...`);
220
+ console.log(` Saved to: .nsaproject.json`);
221
+
222
+ if (doDeploy && !handler.deploymentId) {
223
+ console.log(`\n NOTE: Authorize the DDL handler in the Apps Script editor:`);
224
+ console.log(` https://script.google.com/d/${scriptId}/edit`);
225
+ console.log(` Open the editor → Run any function → Approve permissions.`);
226
+ console.log(` This only needs to be done ONCE per Google account.`);
227
+ }
228
+
229
+ console.log('\nDone.');
230
+ }
231
+
232
+ main();
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Describe a table's columns.
5
+ *
6
+ * Usage:
7
+ * node scripts/describe.mjs --db <name> --table <table> [--env <env>] [--instance <type>]
8
+ */
9
+
10
+ import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, loadTables, requireDataSpreadsheet, requireInstance, ensureAuthenticated } from './lib/utils.mjs';
11
+
12
+ const args = parseArgs();
13
+ const dbName = requireDb(args, 'describe.mjs');
14
+
15
+ if (!args.table) {
16
+ console.error('Usage: node scripts/describe.mjs --db <name> --table <table> [--env <env>] [--instance <type>]');
17
+ process.exit(1);
18
+ }
19
+
20
+ const config = loadDbConfig(dbName);
21
+ const { env, envConfig } = getEnvConfig(config, args.env);
22
+ const instance = requireInstance(args, envConfig);
23
+ const distDir = getDistDirForInstance(dbName, instance.type);
24
+
25
+ const tables = loadTables(dbName);
26
+ const spreadsheetId = requireDataSpreadsheet(tables, dbName);
27
+
28
+ // Auth enforcement
29
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
30
+
31
+ console.log(`Describing table "${args.table}" for ${dbName} (${env}, ${instance.type})...`);
32
+
33
+ const result = claspRun(distDir, 'ddlDescribeTable', [{
34
+ spreadsheetId,
35
+ table: args.table
36
+ }], { userProfile });
37
+
38
+ console.log('\nResult:', JSON.stringify(result, null, 2));
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Drop a table from a data spreadsheet.
5
+ *
6
+ * Usage:
7
+ * node scripts/drop.mjs --db <name> --table <table> [--env <env>] [--instance <type>]
8
+ */
9
+
10
+ import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, loadTables, requireDataSpreadsheet, requireInstance, ensureAuthenticated } from './lib/utils.mjs';
11
+
12
+ const args = parseArgs();
13
+ const dbName = requireDb(args, 'drop.mjs');
14
+
15
+ if (!args.table) {
16
+ console.error('Usage: node scripts/drop.mjs --db <name> --table <table> [--env <env>] [--instance <type>]');
17
+ process.exit(1);
18
+ }
19
+
20
+ const config = loadDbConfig(dbName);
21
+ const { env, envConfig } = getEnvConfig(config, args.env);
22
+ const instance = requireInstance(args, envConfig);
23
+ const distDir = getDistDirForInstance(dbName, instance.type);
24
+
25
+ const tables = loadTables(dbName);
26
+ const spreadsheetId = requireDataSpreadsheet(tables, dbName);
27
+
28
+ // Auth enforcement
29
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
30
+
31
+ console.log(`Dropping table "${args.table}" from ${dbName} (${env}, ${instance.type})...`);
32
+ console.log(` Data spreadsheet: ${spreadsheetId}`);
33
+
34
+ const result = claspRun(distDir, 'ddlDropTable', [{
35
+ spreadsheetId,
36
+ table: args.table
37
+ }], { userProfile });
38
+
39
+ console.log('\nResult:', JSON.stringify(result, null, 2));