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,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Login to Google for a DB project using clasp 3.x named profiles.
5
+ *
6
+ * Usage:
7
+ * node scripts/login.mjs --db <name> [--env <env>] [--account user@gmail.com]
8
+ *
9
+ * Flow:
10
+ * 1. Ask for the Gmail account (or use --account flag / existing config)
11
+ * 2. Run `clasp login --user <dbName>--<env>`
12
+ * 3. Verify the logged-in account matches the specified one
13
+ * 4. Store email in .nsaproject.json → environments.<env>.account
14
+ */
15
+
16
+ import readline from 'readline';
17
+ import { execSync } from 'child_process';
18
+ import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getClaspUserProfile, getAccountEmail, resolveAccount, loadRootConfig, saveRootConfig } from './lib/utils.mjs';
19
+
20
+ function prompt(question) {
21
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
22
+ return new Promise((resolve) => {
23
+ rl.question(question, (answer) => {
24
+ rl.close();
25
+ resolve(answer.trim());
26
+ });
27
+ });
28
+ }
29
+
30
+ const args = parseArgs();
31
+ const dbName = requireDb(args, 'login.mjs');
32
+ const config = loadDbConfig(dbName);
33
+ const { env, envConfig } = getEnvConfig(config, args.env);
34
+
35
+ const profile = getClaspUserProfile(dbName, env);
36
+
37
+ // 1. Determine expected account
38
+ let expectedAccount = args.account || resolveAccount(envConfig, env) || '';
39
+
40
+ if (!expectedAccount) {
41
+ console.log(`\nLogin for ${dbName} (${env})`);
42
+ console.log(` Profile: ${profile}\n`);
43
+ expectedAccount = await prompt('Enter the Gmail account to use: ');
44
+
45
+ if (!expectedAccount) {
46
+ console.error('No account specified. Aborting.');
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ // Basic email validation
52
+ if (!expectedAccount.includes('@')) {
53
+ console.error(`Invalid email: "${expectedAccount}"`);
54
+ process.exit(1);
55
+ }
56
+
57
+ console.log(`\nLogging in as ${expectedAccount} for ${dbName} (${env})...`);
58
+ console.log(` Profile: ${profile}`);
59
+ console.log('\nA browser window will open. Please sign in with the account above.\n');
60
+
61
+ // 2. Run clasp login
62
+ try {
63
+ execSync(`npx --prefer-offline @google/clasp login --user ${profile}`, { stdio: 'inherit' });
64
+ } catch {
65
+ console.error('\nclasp login failed.');
66
+ process.exit(1);
67
+ }
68
+
69
+ // 3. Verify the logged-in account matches
70
+ const actualEmail = await getAccountEmail(profile);
71
+
72
+ if (!actualEmail) {
73
+ console.warn('\nWarning: Could not verify account email (token may not have userinfo scope).');
74
+ console.log('Saving specified account to config anyway.\n');
75
+ const rc1 = loadRootConfig();
76
+ if (!rc1.environments) rc1.environments = {};
77
+ if (!rc1.environments[env]) rc1.environments[env] = {};
78
+ rc1.environments[env].account = expectedAccount;
79
+ saveRootConfig(rc1);
80
+ console.log(`Login complete for ${dbName} (${env})`);
81
+ console.log(` Profile: ${profile}`);
82
+ console.log(` Account (unverified): ${expectedAccount}`);
83
+ process.exit(0);
84
+ }
85
+
86
+ if (actualEmail.toLowerCase() !== expectedAccount.toLowerCase()) {
87
+ console.error(`\nAccount mismatch!`);
88
+ console.error(` Expected: ${expectedAccount}`);
89
+ console.error(` Got: ${actualEmail}`);
90
+ console.error(`\nPlease run login again and sign in with the correct account.`);
91
+ process.exit(1);
92
+ }
93
+
94
+ // 4. Store account in root config
95
+ const rc2 = loadRootConfig();
96
+ if (!rc2.environments) rc2.environments = {};
97
+ if (!rc2.environments[env]) rc2.environments[env] = {};
98
+ rc2.environments[env].account = actualEmail;
99
+ saveRootConfig(rc2);
100
+
101
+ console.log(`\nLogged in as ${actualEmail} for ${dbName} (${env})`);
102
+ console.log(` Profile: ${profile}`);
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Provision tables from tables.json into data spreadsheets.
5
+ *
6
+ * Usage:
7
+ * node scripts/provision.mjs --db <name> [--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, 'provision.mjs');
14
+ const config = loadDbConfig(dbName);
15
+ const { env, envConfig } = getEnvConfig(config, args.env);
16
+ const instance = requireInstance(args, envConfig);
17
+ const distDir = getDistDirForInstance(dbName, instance.type);
18
+
19
+ const tables = loadTables(dbName);
20
+ const spreadsheetId = requireDataSpreadsheet(tables, dbName);
21
+ const tableNames = Object.keys(tables);
22
+
23
+ // Auth enforcement
24
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
25
+
26
+ console.log(`Provisioning ${tableNames.length} table(s) for ${dbName} (${env}, ${instance.type})...`);
27
+ console.log(` Tables: ${tableNames.join(', ')}`);
28
+ console.log(` Data spreadsheet: ${spreadsheetId}`);
29
+
30
+ const result = claspRun(distDir, 'ddlProvisionTables', [{
31
+ spreadsheetId,
32
+ tables
33
+ }], { userProfile });
34
+
35
+ console.log('\nResult:', JSON.stringify(result, null, 2));
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Manually refresh the __sys__tables__ cache and CacheService.
5
+ *
6
+ * Usage:
7
+ * node scripts/refresh-cache.mjs --db <name> [--env <env>] [--instance <type>]
8
+ */
9
+
10
+ import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, requireInstance, ensureAuthenticated } from './lib/utils.mjs';
11
+
12
+ const args = parseArgs();
13
+ const dbName = requireDb(args, 'refresh-cache.mjs');
14
+ const config = loadDbConfig(dbName);
15
+ const { env, envConfig } = getEnvConfig(config, args.env);
16
+ const instance = requireInstance(args, envConfig);
17
+ const distDir = getDistDirForInstance(dbName, instance.type);
18
+
19
+ const sysId = envConfig.systemSpreadsheetId;
20
+ if (!sysId) {
21
+ console.error(`No systemSpreadsheetId set for environment "${env}".`);
22
+ console.error('Run "nsa-sheets-db-builder create -- --db <name>" first.');
23
+ process.exit(1);
24
+ }
25
+
26
+ // Auth enforcement
27
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
28
+
29
+ console.log(`Refreshing cache for ${dbName} (${env}, ${instance.type})...`);
30
+ console.log(` System spreadsheet: ${sysId}`);
31
+
32
+ const result = claspRun(distDir, 'ddlRefreshCache', [{ systemSpreadsheetId: sysId }], { userProfile });
33
+
34
+ console.log('\nResult:', JSON.stringify(result, null, 2));
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Set or rotate the DDL API key (stored in GAS Script Properties).
5
+ * Requires --instance since each GAS project has its own Script Properties.
6
+ *
7
+ * Usage:
8
+ * node scripts/set-key.mjs --db <name> --key <key> [--instance <type>]
9
+ * node scripts/set-key.mjs --db <name> --rotate [--instance <type>]
10
+ * node scripts/set-key.mjs --db <name> --show [--instance <type>]
11
+ */
12
+
13
+ import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, requireInstance, ensureAuthenticated } from './lib/utils.mjs';
14
+
15
+ const args = parseArgs();
16
+ const dbName = requireDb(args, 'set-key.mjs');
17
+ const config = loadDbConfig(dbName);
18
+ const { env, envConfig } = getEnvConfig(config, args.env);
19
+ const instance = requireInstance(args, envConfig);
20
+ const distDir = getDistDirForInstance(dbName, instance.type);
21
+
22
+ // Auth enforcement
23
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
24
+
25
+ if (args.show) {
26
+ console.log(`Checking DDL API key for ${dbName} (${env}, ${instance.type})...`);
27
+ const result = claspRun(distDir, 'ddlGetApiKey', [], { userProfile });
28
+ console.log('\nResult:', JSON.stringify(result, null, 2));
29
+ } else if (args.rotate) {
30
+ console.log(`Rotating DDL API key for ${dbName} (${env}, ${instance.type})...`);
31
+ const result = claspRun(distDir, 'ddlRotateApiKey', [], { userProfile });
32
+ if (result?.key) {
33
+ console.log(`\nNew API key: ${result.key}`);
34
+ console.log('Store this key securely — it will not be shown again.');
35
+ } else {
36
+ console.log('\nResult:', JSON.stringify(result, null, 2));
37
+ }
38
+ } else if (args.key) {
39
+ console.log(`Setting DDL API key for ${dbName} (${env}, ${instance.type})...`);
40
+ const result = claspRun(distDir, 'ddlSetApiKey', [{ key: args.key }], { userProfile });
41
+ console.log('\nResult:', JSON.stringify(result, null, 2));
42
+ } else {
43
+ console.error('Usage:');
44
+ console.error(' node scripts/set-key.mjs --db <name> --key <key> [--instance <type>]');
45
+ console.error(' node scripts/set-key.mjs --db <name> --rotate [--instance <type>]');
46
+ console.error(' node scripts/set-key.mjs --db <name> --show [--instance <type>]');
47
+ process.exit(1);
48
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Set up or remove table-level triggers from tables.json.
5
+ * The system trigger (refreshTableCache) is set up automatically during create.
6
+ *
7
+ * Usage:
8
+ * node scripts/setup-trigger.mjs --db <name> [--instance <type>] Set up all table triggers
9
+ * node scripts/setup-trigger.mjs --db <name> --function <name> Set up one trigger
10
+ * node scripts/setup-trigger.mjs --db <name> --remove [--instance <type>] Remove all triggers
11
+ * node scripts/setup-trigger.mjs --db <name> --remove --function <name> Remove one trigger
12
+ * node scripts/setup-trigger.mjs --db <name> --list List configured triggers
13
+ */
14
+
15
+ import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, requireInstance, ensureAuthenticated, loadTables } from './lib/utils.mjs';
16
+
17
+ const args = parseArgs();
18
+ const dbName = requireDb(args, 'setup-trigger.mjs');
19
+ const config = loadDbConfig(dbName);
20
+ const { env, envConfig } = getEnvConfig(config, args.env);
21
+ const instance = requireInstance(args, envConfig);
22
+ const distDir = getDistDirForInstance(dbName, instance.type);
23
+
24
+ // Collect triggers from all tables
25
+ const tables = loadTables(dbName);
26
+ const triggers = [];
27
+ for (const [tableName, tableDef] of Object.entries(tables)) {
28
+ if (tableDef.triggers && Array.isArray(tableDef.triggers)) {
29
+ for (const t of tableDef.triggers) {
30
+ triggers.push({ ...t, table: tableName });
31
+ }
32
+ }
33
+ }
34
+
35
+ // List configured triggers
36
+ if (args.list) {
37
+ console.log(`Triggers for ${dbName} (${env}):\n`);
38
+ console.log(' System (mandatory):');
39
+ console.log(' refreshTableCache — hourly cache refresh (auto)');
40
+ if (triggers.length === 0) {
41
+ console.log('\n Table triggers: (none)');
42
+ } else {
43
+ console.log('\n Table triggers:');
44
+ for (const t of triggers) {
45
+ console.log(` ${t.function} [${t.table}] — ${t.interval} every ${t.every}`);
46
+ }
47
+ }
48
+ process.exit(0);
49
+ }
50
+
51
+ // Auth enforcement
52
+ const userProfile = await ensureAuthenticated(dbName, args, envConfig, env);
53
+
54
+ if (args.remove) {
55
+ if (args.function) {
56
+ console.log(`Removing trigger "${args.function}" for ${dbName} (${env}, ${instance.type})...`);
57
+ const result = claspRun(distDir, 'ddlRemoveTrigger', [{ functionName: args.function }], { userProfile });
58
+ console.log('\nResult:', JSON.stringify(result, null, 2));
59
+ } else {
60
+ console.log(`Removing all triggers for ${dbName} (${env}, ${instance.type})...`);
61
+ const result = claspRun(distDir, 'ddlRemoveAllTriggers', [], { userProfile });
62
+ console.log('\nResult:', JSON.stringify(result, null, 2));
63
+ }
64
+ } else {
65
+ const toSetup = args.function
66
+ ? triggers.filter(t => t.function === args.function)
67
+ : triggers;
68
+
69
+ if (toSetup.length === 0) {
70
+ if (args.function) {
71
+ console.error(`Trigger "${args.function}" not found in tables.json.`);
72
+ console.error(`Available: ${triggers.map(t => t.function).join(', ') || '(none)'}`);
73
+ } else {
74
+ console.log('No table triggers defined. Add "triggers" array to tables in tables.json.');
75
+ }
76
+ process.exit(0);
77
+ }
78
+
79
+ console.log(`Setting up ${toSetup.length} trigger(s) for ${dbName} (${env}, ${instance.type})...\n`);
80
+
81
+ for (const trigger of toSetup) {
82
+ const intervalMap = { minutes: 'everyMinutes', hours: 'everyHours', days: 'everyDays' };
83
+ const intervalType = intervalMap[trigger.interval] || 'everyHours';
84
+
85
+ console.log(` ${trigger.function} [${trigger.table}]: ${trigger.interval} every ${trigger.every}`);
86
+ const result = claspRun(distDir, 'ddlSetupTrigger', [{
87
+ functionName: trigger.function,
88
+ intervalType,
89
+ interval: trigger.every
90
+ }], { userProfile });
91
+ console.log(` → ${JSON.stringify(result)}`);
92
+ }
93
+
94
+ console.log('\nDone.');
95
+ }