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.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/bin/sheets-deployer.mjs +169 -0
- package/libs/alasql.js +15577 -0
- package/libs/common/gas_response_helper.ts +147 -0
- package/libs/common/gaserror.ts +101 -0
- package/libs/common/gaslogger.ts +172 -0
- package/libs/db_ddl.ts +316 -0
- package/libs/libraries.json +56 -0
- package/libs/spreadsheets_db.ts +4406 -0
- package/libs/triggers.ts +113 -0
- package/package.json +73 -0
- package/scripts/build.mjs +513 -0
- package/scripts/clean.mjs +31 -0
- package/scripts/create.mjs +94 -0
- package/scripts/ddl-handler.mjs +232 -0
- package/scripts/describe.mjs +38 -0
- package/scripts/drop.mjs +39 -0
- package/scripts/init.mjs +465 -0
- package/scripts/lib/utils.mjs +1019 -0
- package/scripts/login.mjs +102 -0
- package/scripts/provision.mjs +35 -0
- package/scripts/refresh-cache.mjs +34 -0
- package/scripts/set-key.mjs +48 -0
- package/scripts/setup-trigger.mjs +95 -0
- package/scripts/setup.mjs +677 -0
- package/scripts/show.mjs +37 -0
- package/scripts/sync.mjs +35 -0
- package/scripts/whoami.mjs +36 -0
- package/src/api/ddl-handler-entry.ts +136 -0
- package/src/api/ddl.ts +321 -0
- package/src/templates/.clasp.json.ejs +1 -0
- package/src/templates/appsscript.json.ejs +16 -0
- package/src/templates/config.ts.ejs +14 -0
- package/src/templates/ddl-handler-config.ts.ejs +3 -0
- package/src/templates/ddl-handler-main.ts.ejs +56 -0
- package/src/templates/main.ts.ejs +288 -0
- package/src/templates/rbac.ts.ejs +148 -0
- package/src/templates/views.ts.ejs +92 -0
- package/templates/blank.json +33 -0
- package/templates/blog-cms.json +507 -0
- package/templates/crm.json +360 -0
- package/templates/e-commerce.json +424 -0
- 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
|
+
}
|