nsa-sheets-db-builder 0.0.1-alpha.2 → 4.0.0-alpha.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/bin/sheets-deployer.mjs +2 -3
- package/package.json +4 -2
- package/scripts/build.mjs +1 -3
- package/scripts/ddl-handler.mjs +20 -16
- package/scripts/init.mjs +27 -40
- package/scripts/lib/utils.mjs +55 -344
- package/scripts/login.mjs +14 -6
- package/scripts/provision.mjs +1 -6
- package/scripts/setup.mjs +18 -23
package/bin/sheets-deployer.mjs
CHANGED
|
@@ -74,7 +74,7 @@ const command = process.argv[2];
|
|
|
74
74
|
const restArgs = process.argv.slice(3);
|
|
75
75
|
|
|
76
76
|
if (!command || command === '--help' || command === '-h') {
|
|
77
|
-
console.log('nsa-sheets-db-builder
|
|
77
|
+
console.log('nsa-sheets-db-builder v4.0.0-alpha.0 — Google Sheets database provisioner\n');
|
|
78
78
|
console.log('Usage: nsa-sheets-db-builder <command> [options]\n');
|
|
79
79
|
console.log('Commands:');
|
|
80
80
|
const maxLen = Math.max(...Object.keys(COMMANDS).map(k => k.length));
|
|
@@ -103,9 +103,8 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
103
103
|
console.log(' dbs/<name>/rbac.json RBAC config (if enabled)');
|
|
104
104
|
console.log(' dbs/<name>/views.json SQL view definitions (if enabled)');
|
|
105
105
|
console.log('\nExamples:');
|
|
106
|
-
console.log(' nsa-sheets-db-builder init --db my-app --template blog-cms
|
|
106
|
+
console.log(' nsa-sheets-db-builder init --db my-app --template blog-cms');
|
|
107
107
|
console.log(' nsa-sheets-db-builder init --db my-app --instances r,w');
|
|
108
|
-
console.log(' nsa-sheets-db-builder init --db my-app --env prod --user prod@gmail.com');
|
|
109
108
|
console.log(' nsa-sheets-db-builder init --db my-app --rbac request --views');
|
|
110
109
|
console.log(' nsa-sheets-db-builder build --db my-app');
|
|
111
110
|
console.log(' nsa-sheets-db-builder push --db my-app --instance rw');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nsa-sheets-db-builder",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "4.0.0-alpha.0",
|
|
4
4
|
"description": "DDL provisioner for Google Sheets — creates spreadsheets, sheets, and headers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,8 +64,10 @@
|
|
|
64
64
|
"ddl-handler": "node scripts/ddl-handler.mjs"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@google/clasp": "^3.2.0",
|
|
68
67
|
"ejs": "^3.1.10",
|
|
69
68
|
"esbuild": "^0.27.3"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"@google/clasp": ">=3.0.0"
|
|
70
72
|
}
|
|
71
73
|
}
|
package/scripts/build.mjs
CHANGED
|
@@ -32,8 +32,7 @@ import {
|
|
|
32
32
|
getAllInstances, requireInstance, loadTables,
|
|
33
33
|
loadRbacConfig, loadViewsConfig, loadCustomMethodsConfig, getFeatures, validateRbacRequirements,
|
|
34
34
|
getDdlHandlerDistDir, getRootEnvConfig,
|
|
35
|
-
PACKAGE_ROOT, COMMON_LIBS_DIR, DBS_DIR, ensureAuthenticated
|
|
36
|
-
recordDeployment
|
|
35
|
+
PACKAGE_ROOT, COMMON_LIBS_DIR, DBS_DIR, ensureAuthenticated
|
|
37
36
|
} from './lib/utils.mjs';
|
|
38
37
|
|
|
39
38
|
// ────────────────────────────────────────
|
|
@@ -498,7 +497,6 @@ async function build() {
|
|
|
498
497
|
execSync(`npx --prefer-offline @google/clasp deploy --description '${desc}'${claspUserFlag(userProfile)}`, { cwd: distDir, stdio: 'inherit' });
|
|
499
498
|
console.log('New deployment created.');
|
|
500
499
|
}
|
|
501
|
-
recordDeployment(dbName, env);
|
|
502
500
|
} catch {
|
|
503
501
|
console.error('clasp deploy failed.');
|
|
504
502
|
process.exit(1);
|
package/scripts/ddl-handler.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Push it to Apps Script
|
|
13
13
|
* - Deploy it as a web app
|
|
14
14
|
*
|
|
15
|
-
* The DDL handler reference is stored in .
|
|
15
|
+
* The DDL handler reference is stored in .nsaproject.json (root config)
|
|
16
16
|
* under environments.<env>.ddlHandler.
|
|
17
17
|
*
|
|
18
18
|
* Usage:
|
|
@@ -41,8 +41,8 @@ import { execSync } from 'child_process';
|
|
|
41
41
|
import {
|
|
42
42
|
parseArgs, requireDb, loadDbConfig, getEnvConfig,
|
|
43
43
|
ensureAuthenticated, resolveAccount,
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
loadRootConfig, saveRootConfig,
|
|
45
|
+
getDdlHandlerDistDir
|
|
46
46
|
} from './lib/utils.mjs';
|
|
47
47
|
import { buildDdlHandler } from './build.mjs';
|
|
48
48
|
|
|
@@ -76,8 +76,8 @@ async function main() {
|
|
|
76
76
|
|
|
77
77
|
const account = resolveAccount(envConfig, env);
|
|
78
78
|
if (!account) {
|
|
79
|
-
console.error('ERROR:
|
|
80
|
-
console.error(`Set
|
|
79
|
+
console.error('ERROR: account not set.');
|
|
80
|
+
console.error(`Set "account" in .nsaproject.json → environments.${env}`);
|
|
81
81
|
process.exit(1);
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -91,9 +91,10 @@ async function main() {
|
|
|
91
91
|
const doPush = args.push || (!args.build && !args.push && !args.deploy);
|
|
92
92
|
const doDeploy = args.deploy || (!args.build && !args.push && !args.deploy);
|
|
93
93
|
|
|
94
|
-
// Load existing handler state from
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// Load existing handler state from root config
|
|
95
|
+
let rootConfig = loadRootConfig();
|
|
96
|
+
let rootEnv = rootConfig.environments?.[env] || {};
|
|
97
|
+
let handler = rootEnv.ddlHandler || {};
|
|
97
98
|
|
|
98
99
|
const ddlAdminKey = handler.ddlAdminKey || generateAdminKey();
|
|
99
100
|
let scriptId = handler.scriptId || '';
|
|
@@ -195,14 +196,17 @@ async function main() {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
// ── Save state to .
|
|
199
|
+
// ── Save state to .nsaproject.json ──
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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);
|
|
206
210
|
|
|
207
211
|
// ── Summary ──
|
|
208
212
|
|
|
@@ -213,7 +217,7 @@ async function main() {
|
|
|
213
217
|
console.log(` Web app: https://script.google.com/macros/s/${deploymentId}/exec`);
|
|
214
218
|
}
|
|
215
219
|
console.log(` Admin key: ${ddlAdminKey.substring(0, 8)}...`);
|
|
216
|
-
console.log(` Saved to: .
|
|
220
|
+
console.log(` Saved to: .nsaproject.json`);
|
|
217
221
|
|
|
218
222
|
if (doDeploy && !handler.deploymentId) {
|
|
219
223
|
console.log(`\n NOTE: Authorize the DDL handler in the Apps Script editor:`);
|
package/scripts/init.mjs
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* node scripts/init.mjs --db <name> --instances r,w
|
|
10
10
|
* node scripts/init.mjs --db <name> --auth simple --rbac
|
|
11
11
|
* node scripts/init.mjs --db <name> --views
|
|
12
|
-
* node scripts/init.mjs --db <name> --env dev --user me@gmail.com
|
|
13
12
|
*
|
|
14
13
|
* Interactive: prompts for template selection if --template not given.
|
|
15
14
|
* Non-interactive: use --template to skip the prompt.
|
|
@@ -25,8 +24,6 @@
|
|
|
25
24
|
* --rbac Enable role-based access control. Scaffolds rbac.json + users/roles tables.
|
|
26
25
|
* Requires --auth simple or --auth google.
|
|
27
26
|
* --views Enable SQL views support. Scaffolds views.json.
|
|
28
|
-
* --env Target environment to set --user for (default: dev)
|
|
29
|
-
* --user Expected Google account email for the environment
|
|
30
27
|
*
|
|
31
28
|
* Creates:
|
|
32
29
|
* dbs/<name>/project.json — all config (name, settings, authMode, features, environments)
|
|
@@ -43,8 +40,7 @@ import readline from 'readline';
|
|
|
43
40
|
import {
|
|
44
41
|
parseArgs, PACKAGE_ROOT, DBS_DIR, COMMON_LIBS_DIR,
|
|
45
42
|
getEnvDefaults, resolveInstanceType,
|
|
46
|
-
loadRootConfig, saveRootConfig
|
|
47
|
-
createEnvFile, saveEnvVar
|
|
43
|
+
loadRootConfig, saveRootConfig
|
|
48
44
|
} from './lib/utils.mjs';
|
|
49
45
|
|
|
50
46
|
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates');
|
|
@@ -119,24 +115,12 @@ function resolveInstances(args) {
|
|
|
119
115
|
if (args.instances && typeof args.instances === 'string') {
|
|
120
116
|
const inputs = args.instances.split(',').map(s => s.trim()).filter(Boolean);
|
|
121
117
|
const types = inputs.map(resolveInstanceType);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
instances[
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (const t of types) typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
129
|
-
|
|
130
|
-
const typeIdx = {};
|
|
131
|
-
for (const type of types) {
|
|
132
|
-
const tag = type.toUpperCase().replace(/-/g, '_');
|
|
133
|
-
if (typeCounts[type] > 1) {
|
|
134
|
-
typeIdx[type] = (typeIdx[type] || 0) + 1;
|
|
135
|
-
instances[`YOUR_${tag}_${typeIdx[type]}_SCRIPT_ID`] = { type, deploymentId: '' };
|
|
136
|
-
} else {
|
|
137
|
-
instances[`YOUR_${tag}_SCRIPT_ID`] = { type, deploymentId: '' };
|
|
138
|
-
}
|
|
139
|
-
}
|
|
118
|
+
for (const type of types) {
|
|
119
|
+
const placeholder = types.length === 1 ? 'YOUR_SCRIPT_ID' : `YOUR_${type.toUpperCase().replace(/-/g, '_')}_SCRIPT_ID`;
|
|
120
|
+
instances[placeholder] = {
|
|
121
|
+
type,
|
|
122
|
+
deploymentId: ''
|
|
123
|
+
};
|
|
140
124
|
}
|
|
141
125
|
} else {
|
|
142
126
|
instances['YOUR_SCRIPT_ID'] = {
|
|
@@ -316,7 +300,7 @@ async function main() {
|
|
|
316
300
|
const args = parseArgs();
|
|
317
301
|
|
|
318
302
|
if (!args.db) {
|
|
319
|
-
console.error('Usage: node scripts/init.mjs --db <name> [--template <template>] [--instances rw,r,w] [--auth noAuth|simple|google] [--rbac] [--views]
|
|
303
|
+
console.error('Usage: node scripts/init.mjs --db <name> [--template <template>] [--instances rw,r,w] [--auth noAuth|simple|google] [--rbac] [--views]');
|
|
320
304
|
process.exit(1);
|
|
321
305
|
}
|
|
322
306
|
|
|
@@ -403,9 +387,6 @@ async function main() {
|
|
|
403
387
|
fs.writeFileSync(path.join(dbDir, 'project.json'), generateProjectJson(dbName, instances, envDefaults, features, authMode));
|
|
404
388
|
fs.writeFileSync(path.join(dbDir, 'tables.json'), generateTablesJson(tables));
|
|
405
389
|
|
|
406
|
-
// Create empty state lock file
|
|
407
|
-
fs.writeFileSync(getStateLockPath(dbName), JSON.stringify({ schemaVersion: 1, environments: {} }, null, 2) + '\n');
|
|
408
|
-
|
|
409
390
|
// RBAC config
|
|
410
391
|
if (features.rbac) {
|
|
411
392
|
fs.writeFileSync(path.join(dbDir, 'rbac.json'), generateRbacJson());
|
|
@@ -436,26 +417,32 @@ async function main() {
|
|
|
436
417
|
// Register project in .nsaproject.json
|
|
437
418
|
const rootConfig = loadRootConfig();
|
|
438
419
|
if (!rootConfig.kind) rootConfig.kind = 'db';
|
|
439
|
-
if (!
|
|
420
|
+
if (!rootConfig.environments) rootConfig.environments = {};
|
|
421
|
+
if (!rootConfig.projects) rootConfig.projects = [];
|
|
422
|
+
|
|
423
|
+
// Ensure env entries exist
|
|
424
|
+
for (const envName of ['dev', 'prod']) {
|
|
425
|
+
if (!rootConfig.environments[envName]) {
|
|
426
|
+
rootConfig.environments[envName] = {
|
|
427
|
+
account: '',
|
|
428
|
+
projectId: '',
|
|
429
|
+
ddlHandler: { scriptId: '', deploymentId: '', ddlAdminKey: '' }
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
440
433
|
|
|
434
|
+
// Store account from env defaults if available and not already set
|
|
435
|
+
if (envDefaults.account && !rootConfig.environments.dev.account) {
|
|
436
|
+
rootConfig.environments.dev.account = envDefaults.account;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Add project to registry if not already listed
|
|
441
440
|
if (!rootConfig.projects.includes(dbName)) {
|
|
442
441
|
rootConfig.projects.push(dbName);
|
|
443
442
|
}
|
|
444
443
|
|
|
445
444
|
saveRootConfig(rootConfig);
|
|
446
445
|
|
|
447
|
-
// Create .env.<env> files if they don't exist
|
|
448
|
-
const targetEnv = args.env || 'dev';
|
|
449
|
-
const userAccount = args.user || envDefaults.account || '';
|
|
450
|
-
|
|
451
|
-
createEnvFile('dev', { account: targetEnv === 'dev' ? userAccount : '' });
|
|
452
|
-
createEnvFile('prod', { account: targetEnv === 'prod' ? userAccount : '' });
|
|
453
|
-
|
|
454
|
-
// If --user provided for a specific env, update that env file
|
|
455
|
-
if (userAccount) {
|
|
456
|
-
saveEnvVar(targetEnv, 'CLASP_USER', userAccount);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
446
|
const tableNames = Object.keys(tables);
|
|
460
447
|
const instanceKeys = Object.keys(instances);
|
|
461
448
|
console.log(`\nCreated DB project: ${dbName}`);
|
package/scripts/lib/utils.mjs
CHANGED
|
@@ -46,7 +46,6 @@ export const DIST_DIR = path.join(PROJECT_ROOT, 'dist');
|
|
|
46
46
|
|
|
47
47
|
// Root project config — shared across all projects under this installation
|
|
48
48
|
const ROOT_CONFIG_PATH = path.join(PROJECT_ROOT, '.nsaproject.json');
|
|
49
|
-
const ROOT_LOCK_PATH = path.join(PROJECT_ROOT, '.nsaproject.lock.json');
|
|
50
49
|
|
|
51
50
|
// DDL Handler dist directory
|
|
52
51
|
const DDL_HANDLER_DIST_NAME = '__ddl-handler__';
|
|
@@ -95,23 +94,18 @@ export function requireDb(args, scriptName) {
|
|
|
95
94
|
// ── Config I/O ───────────────────────────
|
|
96
95
|
//
|
|
97
96
|
// Project layout:
|
|
98
|
-
// dbs/<name>/project.json
|
|
99
|
-
// dbs/<name>/
|
|
100
|
-
// dbs/<name>/
|
|
101
|
-
// dbs/<name>/
|
|
102
|
-
// dbs/<name>/
|
|
103
|
-
// dbs/<name>/
|
|
104
|
-
// dbs/<name>/
|
|
105
|
-
// dbs/<name>/overrides/ — custom method handler files
|
|
97
|
+
// dbs/<name>/project.json — name, settings, features, environments (all-in-one)
|
|
98
|
+
// dbs/<name>/tables.json — table definitions (schema)
|
|
99
|
+
// dbs/<name>/rbac.json — RBAC capabilities + privacy filters (if enabled)
|
|
100
|
+
// dbs/<name>/views.json — SQL view definitions (if enabled)
|
|
101
|
+
// dbs/<name>/customMethods.json — custom method declarations
|
|
102
|
+
// dbs/<name>/libs/ — scaffolded library source files
|
|
103
|
+
// dbs/<name>/overrides/ — custom method handler files
|
|
106
104
|
|
|
107
105
|
export function getProjectConfigPath(dbName) {
|
|
108
106
|
return path.join(DBS_DIR, dbName, 'project.json');
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
export function getStateLockPath(dbName) {
|
|
112
|
-
return path.join(DBS_DIR, dbName, 'state.lock.json');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
109
|
export function getTablesPath(dbName) {
|
|
116
110
|
return path.join(DBS_DIR, dbName, 'tables.json');
|
|
117
111
|
}
|
|
@@ -157,116 +151,13 @@ export function loadDbConfig(dbName) {
|
|
|
157
151
|
}
|
|
158
152
|
|
|
159
153
|
/**
|
|
160
|
-
* Save project.json (config
|
|
154
|
+
* Save project.json (all config: name, settings, features, environments).
|
|
161
155
|
*/
|
|
162
156
|
export function saveProjectConfig(dbName, config) {
|
|
163
157
|
const p = getProjectConfigPath(dbName);
|
|
164
158
|
fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n');
|
|
165
159
|
}
|
|
166
160
|
|
|
167
|
-
// ── State Lock I/O (per-DB) ─────────────
|
|
168
|
-
//
|
|
169
|
-
// state.lock.json — auto-generated metadata, do not edit manually.
|
|
170
|
-
// Tracks schema hashes, deployment timestamps, and migration history.
|
|
171
|
-
// The source of truth for "what's deployed" remains project.json + tables.json.
|
|
172
|
-
//
|
|
173
|
-
// Shape:
|
|
174
|
-
// {
|
|
175
|
-
// schemaVersion: 1,
|
|
176
|
-
// environments: {
|
|
177
|
-
// dev: {
|
|
178
|
-
// lastDeployed: "2026-02-11T16:44:00Z",
|
|
179
|
-
// tables: {
|
|
180
|
-
// "users": { schemaHash: "a3f2c1...", lastProvisioned: "2026-02-11T..." }
|
|
181
|
-
// },
|
|
182
|
-
// migrations: [
|
|
183
|
-
// { id: 1, applied: "2026-02-11T...", description: "Initial schema" }
|
|
184
|
-
// ]
|
|
185
|
-
// }
|
|
186
|
-
// }
|
|
187
|
-
// }
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Load state.lock.json — returns empty structure if not found.
|
|
191
|
-
*/
|
|
192
|
-
export function loadStateLock(dbName) {
|
|
193
|
-
const p = getStateLockPath(dbName);
|
|
194
|
-
if (!fs.existsSync(p)) {
|
|
195
|
-
return { schemaVersion: 1, environments: {} };
|
|
196
|
-
}
|
|
197
|
-
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Save state.lock.json.
|
|
202
|
-
*/
|
|
203
|
-
export function saveStateLock(dbName, state) {
|
|
204
|
-
const p = getStateLockPath(dbName);
|
|
205
|
-
state.lastUpdated = new Date().toISOString();
|
|
206
|
-
fs.writeFileSync(p, JSON.stringify(state, null, 2) + '\n');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get environment state from state.lock.json.
|
|
211
|
-
*/
|
|
212
|
-
export function getEnvState(state, envName) {
|
|
213
|
-
const env = envName || 'dev';
|
|
214
|
-
return state.environments?.[env] || {};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Ensure an environment entry exists in state lock and return it (mutable reference).
|
|
219
|
-
*/
|
|
220
|
-
export function ensureEnvState(state, envName) {
|
|
221
|
-
const env = envName || 'dev';
|
|
222
|
-
if (!state.environments) state.environments = {};
|
|
223
|
-
if (!state.environments[env]) {
|
|
224
|
-
state.environments[env] = {
|
|
225
|
-
lastDeployed: null,
|
|
226
|
-
tables: {},
|
|
227
|
-
migrations: []
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
return state.environments[env];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Compute a simple hash of a table schema for drift detection.
|
|
235
|
-
*/
|
|
236
|
-
export function computeSchemaHash(tableDefinition) {
|
|
237
|
-
const str = JSON.stringify(tableDefinition.schema || {});
|
|
238
|
-
let hash = 0;
|
|
239
|
-
for (let i = 0; i < str.length; i++) {
|
|
240
|
-
const chr = str.charCodeAt(i);
|
|
241
|
-
hash = ((hash << 5) - hash) + chr;
|
|
242
|
-
hash |= 0;
|
|
243
|
-
}
|
|
244
|
-
return Math.abs(hash).toString(36);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Record table provisioning state in the lock file.
|
|
249
|
-
*/
|
|
250
|
-
export function recordTableState(dbName, env, tableName, tableDefinition) {
|
|
251
|
-
const state = loadStateLock(dbName);
|
|
252
|
-
const envState = ensureEnvState(state, env);
|
|
253
|
-
envState.tables[tableName] = {
|
|
254
|
-
schemaHash: computeSchemaHash(tableDefinition),
|
|
255
|
-
lastProvisioned: new Date().toISOString()
|
|
256
|
-
};
|
|
257
|
-
saveStateLock(dbName, state);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Record a deployment in the lock file.
|
|
262
|
-
*/
|
|
263
|
-
export function recordDeployment(dbName, env) {
|
|
264
|
-
const state = loadStateLock(dbName);
|
|
265
|
-
const envState = ensureEnvState(state, env);
|
|
266
|
-
envState.lastDeployed = new Date().toISOString();
|
|
267
|
-
saveStateLock(dbName, state);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
161
|
/**
|
|
271
162
|
* Load tables.json — exits if not found.
|
|
272
163
|
*/
|
|
@@ -370,7 +261,7 @@ export function validateRbacRequirements(tables, rbacConfig) {
|
|
|
370
261
|
|
|
371
262
|
/**
|
|
372
263
|
* Resolve environment config — uses --env flag or defaults to 'dev'.
|
|
373
|
-
* Returns { env, envConfig } where envConfig has instances,
|
|
264
|
+
* Returns { env, envConfig } where envConfig has instances, account, spreadsheetId.
|
|
374
265
|
*/
|
|
375
266
|
export function getEnvConfig(config, envOverride) {
|
|
376
267
|
const env = envOverride || 'dev';
|
|
@@ -431,10 +322,8 @@ export function getDistDirForInstance(dbName, instanceType) {
|
|
|
431
322
|
|
|
432
323
|
// ── Instance Helpers ─────────────────────
|
|
433
324
|
//
|
|
434
|
-
//
|
|
435
|
-
// "instances": { "SCRIPT_ID": { "type": "read-write", "deploymentId": ""
|
|
436
|
-
//
|
|
437
|
-
// Config shape: instances is an array of type strings: ["read-write", "read-only"]
|
|
325
|
+
// Config shape: instances keyed by scriptId, value has type + deploymentId.
|
|
326
|
+
// "instances": { "SCRIPT_ID": { "type": "read-write", "deploymentId": "" } }
|
|
438
327
|
//
|
|
439
328
|
// --instance flag accepts a short alias (rw, r, w), full type, or scriptId.
|
|
440
329
|
|
|
@@ -462,7 +351,7 @@ export function resolveInstanceType(input) {
|
|
|
462
351
|
* Get all instances for an environment config.
|
|
463
352
|
* Each entry: { scriptId, type, deploymentId }
|
|
464
353
|
*
|
|
465
|
-
* @param {object} envConfig - Environment config
|
|
354
|
+
* @param {object} envConfig - Environment config
|
|
466
355
|
* @returns {Array<{ scriptId: string, type: string, deploymentId: string }>}
|
|
467
356
|
*/
|
|
468
357
|
export function getAllInstances(envConfig) {
|
|
@@ -477,8 +366,8 @@ export function getAllInstances(envConfig) {
|
|
|
477
366
|
/**
|
|
478
367
|
* Resolve a single instance by type or scriptId.
|
|
479
368
|
*
|
|
480
|
-
* @param {object} envConfig - Environment config
|
|
481
|
-
* @param {string} identifier - Instance type (e.g. "
|
|
369
|
+
* @param {object} envConfig - Environment config
|
|
370
|
+
* @param {string} identifier - Instance type (e.g. "reader-writer") or scriptId
|
|
482
371
|
* @returns {{ scriptId: string, type: string, deploymentId: string }}
|
|
483
372
|
*/
|
|
484
373
|
export function getInstanceConfig(envConfig, identifier) {
|
|
@@ -511,7 +400,7 @@ export function getInstanceConfig(envConfig, identifier) {
|
|
|
511
400
|
* Require an --instance flag or auto-select if only one instance exists.
|
|
512
401
|
*
|
|
513
402
|
* @param {object} args - Parsed CLI args
|
|
514
|
-
* @param {object} envConfig - Environment config
|
|
403
|
+
* @param {object} envConfig - Environment config
|
|
515
404
|
* @returns {{ scriptId: string, type: string, deploymentId: string }}
|
|
516
405
|
*/
|
|
517
406
|
export function requireInstance(args, envConfig) {
|
|
@@ -812,13 +701,14 @@ export async function getAccountEmail(profile) {
|
|
|
812
701
|
|
|
813
702
|
/**
|
|
814
703
|
* Resolve the account for an environment.
|
|
815
|
-
*
|
|
704
|
+
* Checks DB envConfig first, then root config (.nsaproject.json).
|
|
816
705
|
*
|
|
817
|
-
* @param {object} envConfig - DB environment config
|
|
706
|
+
* @param {object} envConfig - DB environment config
|
|
818
707
|
* @param {string} env - Environment name
|
|
819
708
|
* @returns {string} Account email or empty string
|
|
820
709
|
*/
|
|
821
710
|
export function resolveAccount(envConfig, env) {
|
|
711
|
+
if (envConfig.account) return envConfig.account;
|
|
822
712
|
const rootEnv = getRootEnvConfig(env);
|
|
823
713
|
return rootEnv.account || '';
|
|
824
714
|
}
|
|
@@ -882,7 +772,7 @@ export async function ensureAuthenticated(dbName, args, envConfig, env) {
|
|
|
882
772
|
profile = fallback;
|
|
883
773
|
} else {
|
|
884
774
|
console.error(`\nNo clasp credentials found.`);
|
|
885
|
-
console.error(`Run:
|
|
775
|
+
console.error(`Run: npm run login -- --db ${dbName} --env ${env}`);
|
|
886
776
|
process.exit(1);
|
|
887
777
|
}
|
|
888
778
|
}
|
|
@@ -894,18 +784,22 @@ export async function ensureAuthenticated(dbName, args, envConfig, env) {
|
|
|
894
784
|
console.error(`\nAuth error: ${validation.reason}`);
|
|
895
785
|
if (validation.reason?.includes('mismatch')) {
|
|
896
786
|
console.error(`Logged-in account does not match the declared account for this environment.`);
|
|
897
|
-
console.error(`Run:
|
|
787
|
+
console.error(`Run: npm run login -- --db ${dbName} --env ${env}`);
|
|
898
788
|
} else if (validation.reason?.includes('expired')) {
|
|
899
|
-
console.error(`Run:
|
|
789
|
+
console.error(`Run: npm run login -- --db ${dbName} --env ${env}`);
|
|
900
790
|
}
|
|
901
791
|
process.exit(1);
|
|
902
792
|
}
|
|
903
793
|
|
|
904
|
-
// Store email in
|
|
794
|
+
// Store email in root config if account field was empty
|
|
905
795
|
const knownAccount = resolveAccount(envConfig, env);
|
|
906
796
|
if (!knownAccount && validation.email) {
|
|
907
|
-
|
|
908
|
-
|
|
797
|
+
const rootConfig = loadRootConfig();
|
|
798
|
+
if (!rootConfig.environments) rootConfig.environments = {};
|
|
799
|
+
if (!rootConfig.environments[env]) rootConfig.environments[env] = {};
|
|
800
|
+
rootConfig.environments[env].account = validation.email;
|
|
801
|
+
saveRootConfig(rootConfig);
|
|
802
|
+
console.log(` Stored account "${validation.email}" in .nsaproject.json`);
|
|
909
803
|
}
|
|
910
804
|
|
|
911
805
|
console.log(` Using account: ${validation.email} (profile: ${profile})`);
|
|
@@ -1004,25 +898,7 @@ export function isPlaceholderScriptId(scriptId) {
|
|
|
1004
898
|
return !scriptId || scriptId.startsWith('YOUR_');
|
|
1005
899
|
}
|
|
1006
900
|
|
|
1007
|
-
// ── .env
|
|
1008
|
-
//
|
|
1009
|
-
// Environment-level config lives in .env.<env> files (mandatory).
|
|
1010
|
-
// These are the single source of truth for account, projectId, DDL handler, etc.
|
|
1011
|
-
//
|
|
1012
|
-
// .env.dev:
|
|
1013
|
-
// CLASP_USER=me@gmail.com
|
|
1014
|
-
// GCP_PROJECT_ID=354563759750
|
|
1015
|
-
// DDL_HANDLER_SCRIPT_ID=1abc...
|
|
1016
|
-
// DDL_HANDLER_DEPLOYMENT_ID=AKfyc...
|
|
1017
|
-
// DDL_HANDLER_ADMIN_KEY=a1b2c3...
|
|
1018
|
-
// DRIVE_ROOT_FOLDER_ID= (optional)
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* Get the path to an env file.
|
|
1022
|
-
*/
|
|
1023
|
-
export function getEnvFilePath(env) {
|
|
1024
|
-
return path.join(PROJECT_ROOT, `.env.${env}`);
|
|
1025
|
-
}
|
|
901
|
+
// ── .env File Support ────────────────────
|
|
1026
902
|
|
|
1027
903
|
/**
|
|
1028
904
|
* Load a .env.<env> file from the project root.
|
|
@@ -1032,7 +908,7 @@ export function getEnvFilePath(env) {
|
|
|
1032
908
|
* @returns {object} Key-value pairs, empty object if file missing
|
|
1033
909
|
*/
|
|
1034
910
|
export function loadEnvFile(env) {
|
|
1035
|
-
const envPath =
|
|
911
|
+
const envPath = path.join(PROJECT_ROOT, `.env.${env}`);
|
|
1036
912
|
if (!fs.existsSync(envPath)) {
|
|
1037
913
|
return {};
|
|
1038
914
|
}
|
|
@@ -1063,145 +939,25 @@ export function loadEnvFile(env) {
|
|
|
1063
939
|
}
|
|
1064
940
|
|
|
1065
941
|
/**
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
* @param {string} env - Environment name
|
|
1070
|
-
* @param {string} key - Variable name
|
|
1071
|
-
* @param {string} value - Variable value
|
|
1072
|
-
*/
|
|
1073
|
-
export function saveEnvVar(env, key, value) {
|
|
1074
|
-
const envPath = getEnvFilePath(env);
|
|
1075
|
-
let lines = [];
|
|
1076
|
-
|
|
1077
|
-
if (fs.existsSync(envPath)) {
|
|
1078
|
-
lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Find existing key and update, or append
|
|
1082
|
-
let found = false;
|
|
1083
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1084
|
-
const trimmed = lines[i].trim();
|
|
1085
|
-
if (trimmed.startsWith('#') || !trimmed) continue;
|
|
1086
|
-
const eqIdx = trimmed.indexOf('=');
|
|
1087
|
-
if (eqIdx === -1) continue;
|
|
1088
|
-
if (trimmed.slice(0, eqIdx).trim() === key) {
|
|
1089
|
-
lines[i] = `${key}=${value}`;
|
|
1090
|
-
found = true;
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
if (!found) {
|
|
1096
|
-
// Ensure trailing newline before appending
|
|
1097
|
-
if (lines.length > 0 && lines[lines.length - 1] !== '') {
|
|
1098
|
-
lines.push('');
|
|
1099
|
-
}
|
|
1100
|
-
lines.push(`${key}=${value}`);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Ensure file ends with newline
|
|
1104
|
-
const content = lines.join('\n').replace(/\n*$/, '\n');
|
|
1105
|
-
fs.writeFileSync(envPath, content);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/**
|
|
1109
|
-
* Save multiple key-value pairs to .env.<env> file.
|
|
1110
|
-
*/
|
|
1111
|
-
export function saveEnvVars(env, vars) {
|
|
1112
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
1113
|
-
saveEnvVar(env, key, value);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
/**
|
|
1118
|
-
* Create a .env.<env> file with initial values.
|
|
1119
|
-
* Only creates if file doesn't already exist.
|
|
942
|
+
* Get environment defaults from .env file.
|
|
943
|
+
* These serve as fallbacks for project/api config values.
|
|
1120
944
|
*
|
|
1121
945
|
* @param {string} env - Environment name
|
|
1122
|
-
* @
|
|
1123
|
-
*/
|
|
1124
|
-
export function createEnvFile(env, vars = {}) {
|
|
1125
|
-
const envPath = getEnvFilePath(env);
|
|
1126
|
-
if (fs.existsSync(envPath)) return;
|
|
1127
|
-
|
|
1128
|
-
const lines = [
|
|
1129
|
-
`# .env.${env} — environment config for nsa-sheets-db-builder`,
|
|
1130
|
-
`# Auto-generated. Edit values as needed.`,
|
|
1131
|
-
'',
|
|
1132
|
-
'# Google account used for clasp login',
|
|
1133
|
-
`CLASP_USER=${vars.account || ''}`,
|
|
1134
|
-
'',
|
|
1135
|
-
'# Google Cloud project ID (for GAS deployment)',
|
|
1136
|
-
`GCP_PROJECT_ID=${vars.projectId || ''}`,
|
|
1137
|
-
'',
|
|
1138
|
-
'# DDL Handler (shared per account — auto-populated by setup)',
|
|
1139
|
-
`DDL_HANDLER_SCRIPT_ID=${vars.ddlHandlerScriptId || ''}`,
|
|
1140
|
-
`DDL_HANDLER_DEPLOYMENT_ID=${vars.ddlHandlerDeploymentId || ''}`,
|
|
1141
|
-
`DDL_HANDLER_ADMIN_KEY=${vars.ddlHandlerAdminKey || ''}`,
|
|
1142
|
-
'',
|
|
1143
|
-
'# Optional',
|
|
1144
|
-
`DRIVE_ROOT_FOLDER_ID=${vars.driveFolderId || ''}`,
|
|
1145
|
-
''
|
|
1146
|
-
];
|
|
1147
|
-
|
|
1148
|
-
fs.writeFileSync(envPath, lines.join('\n'));
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* Require an env file to exist, or exit with error.
|
|
1153
|
-
*/
|
|
1154
|
-
export function requireEnvFile(env) {
|
|
1155
|
-
const envPath = getEnvFilePath(env);
|
|
1156
|
-
if (!fs.existsSync(envPath)) {
|
|
1157
|
-
console.error(`Missing .env.${env} file at ${envPath}`);
|
|
1158
|
-
console.error(`Run "nsa-sheets-db-builder init" to create one, or create it manually.`);
|
|
1159
|
-
process.exit(1);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
/**
|
|
1164
|
-
* Get environment config from .env.<env> file.
|
|
1165
|
-
* This is the primary source for env-level vars.
|
|
1166
|
-
*
|
|
1167
|
-
* @param {string} env - Environment name
|
|
1168
|
-
* @returns {{ account, projectId, ddlHandler: { scriptId, deploymentId, ddlAdminKey }, driveFolderId }}
|
|
1169
|
-
*/
|
|
1170
|
-
export function getRootEnvConfig(env) {
|
|
1171
|
-
const vars = loadEnvFile(env);
|
|
1172
|
-
return {
|
|
1173
|
-
account: vars.CLASP_USER || '',
|
|
1174
|
-
projectId: vars.GCP_PROJECT_ID || '',
|
|
1175
|
-
ddlHandler: {
|
|
1176
|
-
scriptId: vars.DDL_HANDLER_SCRIPT_ID || '',
|
|
1177
|
-
deploymentId: vars.DDL_HANDLER_DEPLOYMENT_ID || '',
|
|
1178
|
-
ddlAdminKey: vars.DDL_HANDLER_ADMIN_KEY || ''
|
|
1179
|
-
},
|
|
1180
|
-
driveFolderId: vars.DRIVE_ROOT_FOLDER_ID || ''
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
/**
|
|
1185
|
-
* Get environment defaults from .env file (alias for backward compat).
|
|
946
|
+
* @returns {{ account?: string, driveFolderId?: string, loggingVerbosity?: number }}
|
|
1186
947
|
*/
|
|
1187
948
|
export function getEnvDefaults(env) {
|
|
1188
|
-
const
|
|
949
|
+
const envVars = loadEnvFile(env);
|
|
1189
950
|
const defaults = {};
|
|
1190
951
|
|
|
1191
|
-
if (
|
|
1192
|
-
if (
|
|
952
|
+
if (envVars.DEFAULT_ACCOUNT) defaults.account = envVars.DEFAULT_ACCOUNT;
|
|
953
|
+
if (envVars.DRIVE_ROOT_FOLDER_ID) defaults.driveFolderId = envVars.DRIVE_ROOT_FOLDER_ID;
|
|
954
|
+
if (envVars.DEFAULT_LOGGING_VERBOSITY) defaults.loggingVerbosity = parseInt(envVars.DEFAULT_LOGGING_VERBOSITY, 10);
|
|
955
|
+
if (envVars.DDL_API_KEY) defaults.ddlApiKey = envVars.DDL_API_KEY;
|
|
1193
956
|
|
|
1194
957
|
return defaults;
|
|
1195
958
|
}
|
|
1196
959
|
|
|
1197
960
|
// ── Root Config (.nsaproject.json) ───────
|
|
1198
|
-
//
|
|
1199
|
-
// Project registry only — no environment data (that's in .env.<env> files).
|
|
1200
|
-
// Shape:
|
|
1201
|
-
// {
|
|
1202
|
-
// kind: "db",
|
|
1203
|
-
// projects: ["my-blog", "my-crm"]
|
|
1204
|
-
// }
|
|
1205
961
|
|
|
1206
962
|
/**
|
|
1207
963
|
* Path to the root project config file.
|
|
@@ -1212,10 +968,17 @@ export function getRootConfigPath() {
|
|
|
1212
968
|
|
|
1213
969
|
/**
|
|
1214
970
|
* Load .nsaproject.json — returns empty structure if not found.
|
|
971
|
+
*
|
|
972
|
+
* Shape:
|
|
973
|
+
* {
|
|
974
|
+
* environments: { dev: { account, projectId, ddlHandler: { scriptId, deploymentId, ddlAdminKey } } },
|
|
975
|
+
* projects: { "my-db": { kind: "db" } },
|
|
976
|
+
* settings: { loggingVerbosity: 2 }
|
|
977
|
+
* }
|
|
1215
978
|
*/
|
|
1216
979
|
export function loadRootConfig() {
|
|
1217
980
|
if (!fs.existsSync(ROOT_CONFIG_PATH)) {
|
|
1218
|
-
return { projects:
|
|
981
|
+
return { environments: {}, projects: {}, settings: {} };
|
|
1219
982
|
}
|
|
1220
983
|
return JSON.parse(fs.readFileSync(ROOT_CONFIG_PATH, 'utf8'));
|
|
1221
984
|
}
|
|
@@ -1228,73 +991,21 @@ export function saveRootConfig(rootConfig) {
|
|
|
1228
991
|
}
|
|
1229
992
|
|
|
1230
993
|
/**
|
|
1231
|
-
* Get root
|
|
1232
|
-
*
|
|
994
|
+
* Get the root env config for a given environment.
|
|
995
|
+
* Returns { account, projectId, ddlHandler } or empty object.
|
|
1233
996
|
*/
|
|
1234
|
-
export function
|
|
997
|
+
export function getRootEnvConfig(env) {
|
|
1235
998
|
const rootConfig = loadRootConfig();
|
|
1236
|
-
return rootConfig.
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// ── Root Lock (.nsaproject.lock.json) ────
|
|
1240
|
-
//
|
|
1241
|
-
// Auto-generated metadata — do not edit manually.
|
|
1242
|
-
// Tracks deployment timestamps and DDL handler usage state.
|
|
1243
|
-
// The source of truth for DDL handler config stays in .nsaproject.json.
|
|
1244
|
-
//
|
|
1245
|
-
// Shape:
|
|
1246
|
-
// {
|
|
1247
|
-
// environments: {
|
|
1248
|
-
// dev: {
|
|
1249
|
-
// ddlHandler: { lastDeployed: "2026-02-11T...", lastUsed: "2026-02-11T..." }
|
|
1250
|
-
// }
|
|
1251
|
-
// }
|
|
1252
|
-
// }
|
|
1253
|
-
|
|
1254
|
-
export function getRootLockPath() {
|
|
1255
|
-
return ROOT_LOCK_PATH;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
/**
|
|
1259
|
-
* Load .nsaproject.lock.json — returns empty structure if not found.
|
|
1260
|
-
*/
|
|
1261
|
-
export function loadRootLock() {
|
|
1262
|
-
if (!fs.existsSync(ROOT_LOCK_PATH)) {
|
|
1263
|
-
return { environments: {} };
|
|
1264
|
-
}
|
|
1265
|
-
return JSON.parse(fs.readFileSync(ROOT_LOCK_PATH, 'utf8'));
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
/**
|
|
1269
|
-
* Save .nsaproject.lock.json.
|
|
1270
|
-
*/
|
|
1271
|
-
export function saveRootLock(rootLock) {
|
|
1272
|
-
rootLock.lastUpdated = new Date().toISOString();
|
|
1273
|
-
fs.writeFileSync(ROOT_LOCK_PATH, JSON.stringify(rootLock, null, 2) + '\n');
|
|
999
|
+
return rootConfig.environments?.[env] || {};
|
|
1274
1000
|
}
|
|
1275
1001
|
|
|
1276
1002
|
/**
|
|
1277
|
-
*
|
|
1278
|
-
|
|
1279
|
-
export function recordDdlHandlerDeployment(env) {
|
|
1280
|
-
const rootLock = loadRootLock();
|
|
1281
|
-
if (!rootLock.environments) rootLock.environments = {};
|
|
1282
|
-
if (!rootLock.environments[env]) rootLock.environments[env] = {};
|
|
1283
|
-
if (!rootLock.environments[env].ddlHandler) rootLock.environments[env].ddlHandler = {};
|
|
1284
|
-
rootLock.environments[env].ddlHandler.lastDeployed = new Date().toISOString();
|
|
1285
|
-
saveRootLock(rootLock);
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
/**
|
|
1289
|
-
* Record DDL handler usage in root lock.
|
|
1003
|
+
* Get root settings (loggingVerbosity, etc.).
|
|
1004
|
+
* DB project settings override these.
|
|
1290
1005
|
*/
|
|
1291
|
-
export function
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
if (!rootLock.environments[env]) rootLock.environments[env] = {};
|
|
1295
|
-
if (!rootLock.environments[env].ddlHandler) rootLock.environments[env].ddlHandler = {};
|
|
1296
|
-
rootLock.environments[env].ddlHandler.lastUsed = new Date().toISOString();
|
|
1297
|
-
saveRootLock(rootLock);
|
|
1006
|
+
export function getRootSettings() {
|
|
1007
|
+
const rootConfig = loadRootConfig();
|
|
1008
|
+
return rootConfig.settings || {};
|
|
1298
1009
|
}
|
|
1299
1010
|
|
|
1300
1011
|
// ── DDL Handler ─────────────────────────
|
package/scripts/login.mjs
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* node scripts/login.mjs --db <name> [--env <env>] [--account user@gmail.com]
|
|
8
8
|
*
|
|
9
9
|
* Flow:
|
|
10
|
-
* 1. Ask for the Gmail account (or use --account flag / existing
|
|
10
|
+
* 1. Ask for the Gmail account (or use --account flag / existing config)
|
|
11
11
|
* 2. Run `clasp login --user <dbName>--<env>`
|
|
12
12
|
* 3. Verify the logged-in account matches the specified one
|
|
13
|
-
* 4. Store email in .
|
|
13
|
+
* 4. Store email in .nsaproject.json → environments.<env>.account
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import readline from 'readline';
|
|
17
17
|
import { execSync } from 'child_process';
|
|
18
|
-
import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getClaspUserProfile, getAccountEmail, resolveAccount,
|
|
18
|
+
import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getClaspUserProfile, getAccountEmail, resolveAccount, loadRootConfig, saveRootConfig } from './lib/utils.mjs';
|
|
19
19
|
|
|
20
20
|
function prompt(question) {
|
|
21
21
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -72,7 +72,11 @@ const actualEmail = await getAccountEmail(profile);
|
|
|
72
72
|
if (!actualEmail) {
|
|
73
73
|
console.warn('\nWarning: Could not verify account email (token may not have userinfo scope).');
|
|
74
74
|
console.log('Saving specified account to config anyway.\n');
|
|
75
|
-
|
|
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);
|
|
76
80
|
console.log(`Login complete for ${dbName} (${env})`);
|
|
77
81
|
console.log(` Profile: ${profile}`);
|
|
78
82
|
console.log(` Account (unverified): ${expectedAccount}`);
|
|
@@ -87,8 +91,12 @@ if (actualEmail.toLowerCase() !== expectedAccount.toLowerCase()) {
|
|
|
87
91
|
process.exit(1);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
// 4. Store account in
|
|
91
|
-
|
|
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);
|
|
92
100
|
|
|
93
101
|
console.log(`\nLogged in as ${actualEmail} for ${dbName} (${env})`);
|
|
94
102
|
console.log(` Profile: ${profile}`);
|
package/scripts/provision.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* node scripts/provision.mjs --db <name> [--env <env>] [--instance <type>]
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, loadTables, requireDataSpreadsheet, requireInstance, ensureAuthenticated
|
|
10
|
+
import { parseArgs, requireDb, loadDbConfig, getEnvConfig, getDistDirForInstance, claspRun, loadTables, requireDataSpreadsheet, requireInstance, ensureAuthenticated } from './lib/utils.mjs';
|
|
11
11
|
|
|
12
12
|
const args = parseArgs();
|
|
13
13
|
const dbName = requireDb(args, 'provision.mjs');
|
|
@@ -33,8 +33,3 @@ const result = claspRun(distDir, 'ddlProvisionTables', [{
|
|
|
33
33
|
}], { userProfile });
|
|
34
34
|
|
|
35
35
|
console.log('\nResult:', JSON.stringify(result, null, 2));
|
|
36
|
-
|
|
37
|
-
// Record table state in lock file
|
|
38
|
-
for (const [tableName, tableDef] of Object.entries(tables)) {
|
|
39
|
-
recordTableState(dbName, env, tableName, tableDef);
|
|
40
|
-
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -36,10 +36,8 @@ import {
|
|
|
36
36
|
getFeatures, validateRbacRequirements,
|
|
37
37
|
ensureAuthenticated, isPlaceholderScriptId,
|
|
38
38
|
getDataSpreadsheetIds,
|
|
39
|
-
resolveAccount,
|
|
40
|
-
getDdlHandlerDistDir
|
|
41
|
-
recordDdlHandlerDeployment, recordDeployment, recordTableState,
|
|
42
|
-
getRootEnvConfig, saveEnvVars
|
|
39
|
+
loadRootConfig, saveRootConfig, resolveAccount,
|
|
40
|
+
getDdlHandlerDistDir
|
|
43
41
|
} from './lib/utils.mjs';
|
|
44
42
|
import { buildInstance, buildDdlHandler, getDeploymentDescription } from './build.mjs';
|
|
45
43
|
|
|
@@ -246,14 +244,15 @@ async function setup() {
|
|
|
246
244
|
|
|
247
245
|
const account = resolveAccount(envConfig, env);
|
|
248
246
|
if (!account) {
|
|
249
|
-
console.error(' ERROR:
|
|
250
|
-
console.error(
|
|
247
|
+
console.error(' ERROR: account not set.');
|
|
248
|
+
console.error(' Set "account" in .nsaproject.json → environments.' + env);
|
|
251
249
|
console.error(' Or run login first to auto-detect.');
|
|
252
250
|
process.exit(1);
|
|
253
251
|
}
|
|
254
252
|
|
|
255
|
-
// Resolve DDL handler from .
|
|
256
|
-
|
|
253
|
+
// Resolve DDL handler from root config (.nsaproject.json)
|
|
254
|
+
let rootConfig = loadRootConfig();
|
|
255
|
+
let rootEnv = rootConfig.environments?.[env] || {};
|
|
257
256
|
let ddlHandler = rootEnv.ddlHandler || {};
|
|
258
257
|
|
|
259
258
|
if (ddlHandler.scriptId && ddlHandler.deploymentId) {
|
|
@@ -342,16 +341,18 @@ async function setup() {
|
|
|
342
341
|
console.log(` deploymentId: ${handlerDeploymentId}`);
|
|
343
342
|
}
|
|
344
343
|
|
|
345
|
-
// f. Save to .
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
344
|
+
// f. Save to .nsaproject.json
|
|
345
|
+
if (!rootConfig.environments) rootConfig.environments = {};
|
|
346
|
+
if (!rootConfig.environments[env]) rootConfig.environments[env] = {};
|
|
347
|
+
rootConfig.environments[env].ddlHandler = {
|
|
348
|
+
scriptId: handlerScriptId,
|
|
349
|
+
deploymentId: handlerDeploymentId,
|
|
350
|
+
ddlAdminKey: handlerAdminKey
|
|
351
|
+
};
|
|
352
|
+
saveRootConfig(rootConfig);
|
|
353
|
+
ddlHandler = rootConfig.environments[env].ddlHandler;
|
|
353
354
|
|
|
354
|
-
console.log(`\n DDL handler saved to .
|
|
355
|
+
console.log(`\n DDL handler saved to .nsaproject.json`);
|
|
355
356
|
|
|
356
357
|
// g. Print authorization reminder
|
|
357
358
|
console.log(`\n IMPORTANT: Authorize the DDL handler in the Apps Script editor:`);
|
|
@@ -570,7 +571,6 @@ async function setup() {
|
|
|
570
571
|
process.exit(1);
|
|
571
572
|
}
|
|
572
573
|
}
|
|
573
|
-
recordDeployment(dbName, env);
|
|
574
574
|
console.log('\n Re-build + re-push + re-deploy complete.');
|
|
575
575
|
}
|
|
576
576
|
|
|
@@ -604,11 +604,6 @@ async function setup() {
|
|
|
604
604
|
}
|
|
605
605
|
console.log(' Provision result:', JSON.stringify(provisionResult, null, 2));
|
|
606
606
|
|
|
607
|
-
// Record table state in lock file
|
|
608
|
-
for (const [tableName, tableDef] of Object.entries(tables)) {
|
|
609
|
-
recordTableState(dbName, env, tableName, tableDef);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
607
|
// ── Step 10: Rotate API keys (via DB script web app) ──
|
|
613
608
|
|
|
614
609
|
step(10, 'Rotate API keys');
|