relight-cli 0.2.0 → 0.3.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.
@@ -1,84 +1,126 @@
1
1
  import { phase, status, success, fatal, hint, fmt, table } from "../lib/output.js";
2
- import { resolveAppName, resolveDb, readLink, linkApp } from "../lib/link.js";
2
+ import { resolveAppName, readLink, linkApp } from "../lib/link.js";
3
3
  import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
4
+ import {
5
+ getDatabaseConfig, saveDatabaseConfig, removeDatabaseConfig, listDatabases,
6
+ tryGetServiceConfig, normalizeServiceConfig, CLOUD_IDS,
7
+ } from "../lib/config.js";
4
8
  import { createInterface } from "readline";
5
9
  import { readFileSync, writeFileSync } from "fs";
6
10
 
7
- // Resolve app cloud and db cloud from options + .relight.yaml
8
- function resolveDbClouds(options) {
9
- var appCloud = resolveCloudId(options.cloud);
10
- var dbFlag = options.db || resolveDb();
11
- var crossCloud = dbFlag && resolveCloudId(dbFlag) !== appCloud;
12
- var dbCloud = crossCloud ? resolveCloudId(dbFlag) : appCloud;
13
- return { appCloud, dbCloud, crossCloud };
11
+ // --- Helpers ---
12
+
13
+ function resolveDatabase(name) {
14
+ if (!name) {
15
+ var linked = readLink();
16
+ name = linked?.db;
17
+ }
18
+ if (!name) fatal("No database specified.");
19
+ var entry = getDatabaseConfig(name);
20
+ if (!entry) fatal(`Database '${name}' not found. Run ${fmt.cmd("relight db list")} to see databases.`);
21
+ return { name, entry };
14
22
  }
15
23
 
16
- export async function dbCreate(name, options) {
17
- name = resolveAppName(name);
18
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
19
- var dbCfg = getCloudCfg(dbCloud);
20
- var dbProvider = await getProvider(dbCloud, "db");
24
+ async function loadProvider(entry) {
25
+ var providerId = entry.provider;
21
26
 
22
- phase("Creating database");
23
- if (options.jurisdiction) status(`relight-${name} (jurisdiction: ${options.jurisdiction})...`);
24
- else if (options.location) status(`relight-${name} (location: ${options.location})...`);
25
- else status(`relight-${name}...`);
27
+ // Check if it's a service
28
+ var service = tryGetServiceConfig(providerId);
29
+ if (service && service.layer === "db") {
30
+ var provider = await import(`../lib/providers/${service.type}/db.js`);
31
+ var cfg = { ...normalizeServiceConfig(service), serviceName: providerId };
32
+ return { provider, cfg };
33
+ }
26
34
 
27
- var result;
28
- try {
29
- result = await dbProvider.createDatabase(dbCfg, name, {
30
- location: options.location,
31
- jurisdiction: options.jurisdiction,
32
- skipAppConfig: crossCloud,
33
- });
34
- } catch (e) {
35
- fatal(e.message);
35
+ // It's a cloud
36
+ var provider = await getProvider(providerId, "db");
37
+ var cfg = getCloudCfg(providerId);
38
+ return { provider, cfg };
39
+ }
40
+
41
+ function resolveProvider(options) {
42
+ var provider = options.provider;
43
+ if (!provider) {
44
+ var linked = readLink();
45
+ // Try to infer from linked cloud
46
+ if (linked?.cloud) provider = linked.cloud;
47
+ }
48
+ if (!provider) {
49
+ fatal(
50
+ "No provider specified.",
51
+ `Use ${fmt.cmd("--provider <cf|gcp|aws|service-name>")} to specify the database provider.`
52
+ );
36
53
  }
54
+ return provider;
55
+ }
37
56
 
38
- // Cross-cloud: inject DB env vars into the app cloud's config
39
- if (crossCloud) {
40
- var appCfg = getCloudCfg(appCloud);
41
- var appProvider = await getProvider(appCloud, "app");
42
- status(`Injecting DB config into ${appCloud} app...`);
57
+ // --- Commands ---
43
58
 
44
- var appConfig = await appProvider.getAppConfig(appCfg, name);
45
- if (!appConfig) {
46
- fatal(`App ${name} not found on ${appCloud}.`);
47
- }
59
+ export async function dbCreate(name, options) {
60
+ if (!name) fatal("Database name is required.", `Usage: relight db create <name> --provider <provider>`);
48
61
 
49
- appConfig.dbId = result.dbId;
50
- appConfig.dbName = result.dbName;
62
+ // Check if already exists
63
+ if (getDatabaseConfig(name)) {
64
+ fatal(`Database '${name}' already exists.`);
65
+ }
51
66
 
52
- if (!appConfig.envKeys) appConfig.envKeys = [];
53
- if (!appConfig.secretKeys) appConfig.secretKeys = [];
54
- if (!appConfig.env) appConfig.env = {};
67
+ var providerId = resolveProvider(options);
55
68
 
56
- if (result.connectionUrl) {
57
- // CF uses DB_URL, GCP/AWS use DATABASE_URL
58
- var urlKey = dbCloud === "cf" ? "DB_URL" : "DATABASE_URL";
59
- appConfig.env[urlKey] = result.connectionUrl;
60
- if (!appConfig.envKeys.includes(urlKey)) appConfig.envKeys.push(urlKey);
69
+ // Determine if this is a service or cloud
70
+ var service = tryGetServiceConfig(providerId);
71
+ var isService = service && service.layer === "db";
72
+ var isPostgres;
73
+ var provider;
74
+ var cfg;
75
+
76
+ if (isService) {
77
+ provider = await import(`../lib/providers/${service.type}/db.js`);
78
+ cfg = { ...normalizeServiceConfig(service), serviceName: providerId };
79
+ isPostgres = true;
80
+ } else {
81
+ if (!CLOUD_IDS.includes(providerId)) {
82
+ fatal(
83
+ `Unknown provider: ${providerId}`,
84
+ `Supported: ${CLOUD_IDS.join(", ")} or a registered db service name.`
85
+ );
61
86
  }
87
+ provider = await getProvider(providerId, "db");
88
+ cfg = getCloudCfg(providerId);
89
+ isPostgres = providerId !== "cf";
90
+ }
62
91
 
63
- appConfig.env["DB_TOKEN"] = "[hidden]";
64
- appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
65
- appConfig.secretKeys.push("DB_TOKEN");
66
- appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
92
+ phase("Creating database");
93
+ if (options.jurisdiction) status(`${name} (jurisdiction: ${options.jurisdiction})...`);
94
+ else if (options.location) status(`${name} (location: ${options.location})...`);
95
+ else status(`${name}...`);
67
96
 
68
- await appProvider.pushAppConfig(appCfg, name, appConfig, {
69
- newSecrets: { DB_TOKEN: result.dbToken },
97
+ var result;
98
+ try {
99
+ result = await provider.createDatabase(cfg, name, {
100
+ location: options.location,
101
+ jurisdiction: options.jurisdiction,
70
102
  });
71
-
72
- // Persist db cloud in .relight.yaml
73
- var linked = readLink();
74
- if (linked && !linked.db) {
75
- linkApp(linked.app, linked.cloud, linked.dns, dbCloud);
76
- }
103
+ } catch (e) {
104
+ fatal(e.message);
77
105
  }
78
106
 
107
+ // Save to database registry
108
+ saveDatabaseConfig(name, {
109
+ provider: providerId,
110
+ dbId: result.dbId,
111
+ dbName: result.dbName,
112
+ dbUser: result.dbUser || null,
113
+ dbToken: result.dbToken,
114
+ connectionUrl: result.connectionUrl,
115
+ isPostgres,
116
+ apps: [],
117
+ createdAt: new Date().toISOString(),
118
+ });
119
+
79
120
  if (options.json) {
80
121
  console.log(JSON.stringify({
81
122
  name,
123
+ provider: providerId,
82
124
  dbId: result.dbId,
83
125
  dbName: result.dbName,
84
126
  dbToken: result.dbToken,
@@ -87,22 +129,21 @@ export async function dbCreate(name, options) {
87
129
  return;
88
130
  }
89
131
 
90
- success(`Database ${fmt.app(result.dbName)} created!`);
132
+ success(`Database ${fmt.app(name)} created!`);
133
+ console.log(` ${fmt.bold("Provider:")} ${providerId}`);
91
134
  console.log(` ${fmt.bold("DB ID:")} ${result.dbId}`);
92
135
  console.log(` ${fmt.bold("DB Name:")} ${result.dbName}`);
93
136
  if (result.connectionUrl) {
94
137
  console.log(` ${fmt.bold("DB URL:")} ${fmt.url(result.connectionUrl)}`);
95
138
  }
96
139
  console.log(` ${fmt.bold("Token:")} ${result.dbToken}`);
97
- if (crossCloud) {
98
- console.log(` ${fmt.bold("DB Cloud:")} ${fmt.cloud(dbCloud)}`);
99
- }
100
- hint("Next", `relight db shell ${name}`);
140
+ hint("Next", `relight db attach ${name} <app>`);
101
141
  }
102
142
 
103
143
  export async function dbDestroy(name, options) {
104
- name = resolveAppName(name);
105
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
144
+ var resolved = resolveDatabase(name);
145
+ name = resolved.name;
146
+ var entry = resolved.entry;
106
147
 
107
148
  if (options.confirm !== name) {
108
149
  if (process.stdin.isTTY) {
@@ -122,77 +163,251 @@ export async function dbDestroy(name, options) {
122
163
  }
123
164
  }
124
165
 
166
+ // Auto-detach from all attached apps
167
+ if (entry.apps && entry.apps.length > 0) {
168
+ for (var appName of entry.apps) {
169
+ process.stderr.write(` Detaching from ${fmt.app(appName)}...\n`);
170
+ try {
171
+ await detachFromApp(entry, appName);
172
+ } catch (e) {
173
+ process.stderr.write(` ${fmt.dim(`Warning: could not detach from ${appName}: ${e.message}`)}\n`);
174
+ }
175
+ }
176
+ }
177
+
125
178
  phase("Destroying database");
126
179
 
127
- if (crossCloud) {
128
- // Read dbId from app cloud config
129
- var appCfg = getCloudCfg(appCloud);
130
- var appProvider = await getProvider(appCloud, "app");
131
- var appConfig = await appProvider.getAppConfig(appCfg, name);
132
- if (!appConfig || !appConfig.dbId) {
133
- fatal(`App ${name} does not have a database.`);
134
- }
180
+ var { provider, cfg } = await loadProvider(entry);
181
+ try {
182
+ await provider.destroyDatabase(cfg, name, { dbId: entry.dbId });
183
+ } catch (e) {
184
+ fatal(e.message);
185
+ }
135
186
 
136
- var dbId = appConfig.dbId;
137
- var dbCfg = getCloudCfg(dbCloud);
138
- var dbProvider = await getProvider(dbCloud, "db");
187
+ removeDatabaseConfig(name);
188
+ success(`Database ${fmt.app(name)} destroyed.`);
189
+ }
139
190
 
140
- // Destroy DB on db cloud
141
- try {
142
- await dbProvider.destroyDatabase(dbCfg, name, { dbId });
143
- } catch (e) {
144
- fatal(e.message);
145
- }
191
+ export async function dbList(options) {
192
+ var databases = listDatabases();
193
+
194
+ if (options.json) {
195
+ console.log(JSON.stringify(databases, null, 2));
196
+ return;
197
+ }
198
+
199
+ if (databases.length === 0) {
200
+ console.log(fmt.dim("\n No databases. Create one with: relight db create <name> --provider <provider>\n"));
201
+ return;
202
+ }
203
+
204
+ var cols = ["NAME", "PROVIDER", "DB NAME", "APPS", "CREATED"];
205
+ var rows = databases.map((db) => [
206
+ db.name,
207
+ db.provider,
208
+ db.dbName || "-",
209
+ (db.apps || []).join(", ") || "-",
210
+ db.createdAt ? db.createdAt.split("T")[0] : "-",
211
+ ]);
212
+
213
+ console.log(table(cols, rows));
214
+ }
215
+
216
+ export async function dbAttach(name, appName, options) {
217
+ var resolved = resolveDatabase(name);
218
+ name = resolved.name;
219
+ var entry = resolved.entry;
146
220
 
147
- // Clean up app config on app cloud
148
- status(`Cleaning up app config on ${appCloud}...`);
149
- delete appConfig.dbId;
150
- delete appConfig.dbName;
221
+ appName = resolveAppName(appName);
151
222
 
152
- if (appConfig.env) {
153
- delete appConfig.env["DB_URL"];
154
- delete appConfig.env["DB_TOKEN"];
155
- delete appConfig.env["DATABASE_URL"];
223
+ // Check not already attached
224
+ if (entry.apps && entry.apps.includes(appName)) {
225
+ fatal(`Database '${name}' is already attached to '${appName}'.`);
226
+ }
227
+
228
+ // Resolve app's cloud/compute
229
+ var appCloud = resolveCloudId(options.cloud);
230
+ var appCfg = getCloudCfg(appCloud);
231
+ var appProvider = await getProvider(appCloud, "app");
232
+
233
+ // Check if compute service
234
+ if (options.compute) {
235
+ var computeService = tryGetServiceConfig(options.compute);
236
+ if (computeService) {
237
+ appProvider = await import(`../lib/providers/${computeService.type}/app.js`);
238
+ appCfg = normalizeServiceConfig(computeService);
156
239
  }
157
- if (appConfig.envKeys) appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_URL" && k !== "DATABASE_URL");
158
- if (appConfig.secretKeys) appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
240
+ }
159
241
 
160
- await appProvider.pushAppConfig(appCfg, name, appConfig);
242
+ phase("Attaching database");
243
+ status(`${name} -> ${appName}...`);
244
+
245
+ var appConfig = await appProvider.getAppConfig(appCfg, appName);
246
+ if (!appConfig) {
247
+ fatal(`App ${appName} not found.`);
248
+ }
249
+
250
+ if (!appConfig.envKeys) appConfig.envKeys = [];
251
+ if (!appConfig.secretKeys) appConfig.secretKeys = [];
252
+ if (!appConfig.env) appConfig.env = {};
253
+
254
+ // Inject env vars
255
+ if (entry.isPostgres) {
256
+ if (entry.connectionUrl) {
257
+ appConfig.env["DATABASE_URL"] = entry.connectionUrl;
258
+ if (!appConfig.envKeys.includes("DATABASE_URL")) appConfig.envKeys.push("DATABASE_URL");
259
+ }
161
260
  } else {
162
- var dbCfg = getCloudCfg(dbCloud);
163
- var dbProvider = await getProvider(dbCloud, "db");
164
- try {
165
- await dbProvider.destroyDatabase(dbCfg, name);
166
- } catch (e) {
167
- fatal(e.message);
261
+ // CF D1
262
+ appConfig.dbId = entry.dbId;
263
+ appConfig.dbName = entry.dbName;
264
+ if (entry.connectionUrl) {
265
+ appConfig.env["DB_URL"] = entry.connectionUrl;
266
+ if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
168
267
  }
169
268
  }
170
269
 
171
- success(`Database for ${fmt.app(name)} destroyed.`);
270
+ appConfig.env["DB_TOKEN"] = "[hidden]";
271
+ appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
272
+ appConfig.secretKeys.push("DB_TOKEN");
273
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
274
+
275
+ if (entry.dbUser) appConfig.dbUser = entry.dbUser;
276
+
277
+ await appProvider.pushAppConfig(appCfg, appName, appConfig, {
278
+ newSecrets: { DB_TOKEN: entry.dbToken },
279
+ });
280
+
281
+ // Update registry: add app to entry.apps
282
+ if (!entry.apps) entry.apps = [];
283
+ entry.apps.push(appName);
284
+ saveDatabaseConfig(name, entry);
285
+
286
+ // Update .relight.yaml: set db to database name
287
+ var linked = readLink();
288
+ if (linked && linked.app === appName) {
289
+ linkApp(linked.app, linked.cloud, linked.dns, name, linked.compute);
290
+ }
291
+
292
+ success(`Database ${fmt.app(name)} attached to ${fmt.app(appName)}.`);
172
293
  }
173
294
 
174
- // In cross-cloud mode, read dbId from app cloud config
175
- async function getDbIdFromAppCloud(appCloud, name) {
295
+ // Helper to detach a database from an app (used by dbDetach and dbDestroy)
296
+ async function detachFromApp(entry, appName, options = {}) {
297
+ var appCloud = options.cloud ? resolveCloudId(options.cloud) : null;
298
+ if (!appCloud) {
299
+ var linked = readLink();
300
+ appCloud = linked?.cloud;
301
+ }
302
+ if (!appCloud) {
303
+ // Try to infer from entry.provider if it's a cloud
304
+ if (CLOUD_IDS.includes(entry.provider)) {
305
+ appCloud = entry.provider;
306
+ }
307
+ }
308
+ if (!appCloud) {
309
+ throw new Error("Cannot determine app cloud. Use --cloud to specify.");
310
+ }
311
+
176
312
  var appCfg = getCloudCfg(appCloud);
177
313
  var appProvider = await getProvider(appCloud, "app");
178
- var appConfig = await appProvider.getAppConfig(appCfg, name);
179
- if (!appConfig || !appConfig.dbId) {
180
- throw new Error(`App ${name} does not have a database.`);
314
+
315
+ if (options.compute) {
316
+ var computeService = tryGetServiceConfig(options.compute);
317
+ if (computeService) {
318
+ appProvider = await import(`../lib/providers/${computeService.type}/app.js`);
319
+ appCfg = normalizeServiceConfig(computeService);
320
+ }
321
+ }
322
+
323
+ var appConfig = await appProvider.getAppConfig(appCfg, appName);
324
+ if (!appConfig) return;
325
+
326
+ // Remove DB env vars
327
+ delete appConfig.dbId;
328
+ delete appConfig.dbName;
329
+ delete appConfig.dbUser;
330
+
331
+ if (appConfig.env) {
332
+ delete appConfig.env["DB_URL"];
333
+ delete appConfig.env["DB_TOKEN"];
334
+ delete appConfig.env["DATABASE_URL"];
181
335
  }
182
- return appConfig.dbId;
336
+ if (appConfig.envKeys) {
337
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_URL" && k !== "DATABASE_URL");
338
+ }
339
+ if (appConfig.secretKeys) {
340
+ appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
341
+ }
342
+
343
+ await appProvider.pushAppConfig(appCfg, appName, appConfig);
344
+ }
345
+
346
+ export async function dbDetach(appName, options) {
347
+ appName = resolveAppName(appName);
348
+
349
+ // Find which database is attached to this app
350
+ var databases = listDatabases();
351
+ var attached = null;
352
+ var attachedName = null;
353
+
354
+ // Check .relight.yaml first
355
+ var linked = readLink();
356
+ if (linked?.db) {
357
+ var entry = getDatabaseConfig(linked.db);
358
+ if (entry && entry.apps && entry.apps.includes(appName)) {
359
+ attached = entry;
360
+ attachedName = linked.db;
361
+ }
362
+ }
363
+
364
+ // Search registry
365
+ if (!attached) {
366
+ for (var db of databases) {
367
+ if (db.apps && db.apps.includes(appName)) {
368
+ attached = db;
369
+ attachedName = db.name;
370
+ break;
371
+ }
372
+ }
373
+ }
374
+
375
+ if (!attached) {
376
+ fatal(`No database found attached to '${appName}'.`);
377
+ }
378
+
379
+ phase("Detaching database");
380
+ status(`${attachedName} from ${appName}...`);
381
+
382
+ try {
383
+ await detachFromApp(attached, appName, options);
384
+ } catch (e) {
385
+ fatal(e.message);
386
+ }
387
+
388
+ // Update registry: remove app from entry.apps
389
+ attached.apps = (attached.apps || []).filter((a) => a !== appName);
390
+ // Remove extra fields added by listDatabases() (like 'name')
391
+ var cleanEntry = getDatabaseConfig(attachedName);
392
+ cleanEntry.apps = attached.apps;
393
+ saveDatabaseConfig(attachedName, cleanEntry);
394
+
395
+ success(`Database ${fmt.app(attachedName)} detached from ${fmt.app(appName)}.`);
183
396
  }
184
397
 
185
398
  export async function dbInfo(name, options) {
186
- name = resolveAppName(name);
187
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
188
- var dbCfg = getCloudCfg(dbCloud);
189
- var dbProvider = await getProvider(dbCloud, "db");
399
+ var resolved = resolveDatabase(name);
400
+ name = resolved.name;
401
+ var entry = resolved.entry;
190
402
 
191
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
403
+ var { provider, cfg } = await loadProvider(entry);
192
404
 
193
405
  var info;
194
406
  try {
195
- info = await dbProvider.getDatabaseInfo(dbCfg, name, { dbId });
407
+ info = await provider.getDatabaseInfo(cfg, name, {
408
+ dbId: entry.dbId,
409
+ connectionUrl: entry.connectionUrl,
410
+ });
196
411
  } catch (e) {
197
412
  fatal(e.message);
198
413
  }
@@ -200,19 +415,23 @@ export async function dbInfo(name, options) {
200
415
  if (options.json) {
201
416
  console.log(JSON.stringify({
202
417
  name,
418
+ provider: entry.provider,
203
419
  dbId: info.dbId,
204
420
  dbName: info.dbName,
205
421
  connectionUrl: info.connectionUrl,
206
422
  size: info.size,
207
423
  numTables: info.numTables,
208
- createdAt: info.createdAt,
424
+ apps: entry.apps || [],
425
+ createdAt: info.createdAt || entry.createdAt,
209
426
  }, null, 2));
210
427
  return;
211
428
  }
212
429
 
213
430
  console.log("");
214
- console.log(`${fmt.bold("Database:")} ${fmt.app(info.dbName)}`);
431
+ console.log(`${fmt.bold("Database:")} ${fmt.app(name)}`);
432
+ console.log(`${fmt.bold("Provider:")} ${entry.provider}`);
215
433
  console.log(`${fmt.bold("DB ID:")} ${info.dbId}`);
434
+ console.log(`${fmt.bold("DB Name:")} ${info.dbName}`);
216
435
  if (info.size != null) {
217
436
  var sizeKb = (info.size / 1024).toFixed(1);
218
437
  console.log(`${fmt.bold("Size:")} ${sizeKb} KB`);
@@ -224,34 +443,41 @@ export async function dbInfo(name, options) {
224
443
  console.log(`${fmt.bold("DB URL:")} ${fmt.url(info.connectionUrl)}`);
225
444
  }
226
445
  console.log(`${fmt.bold("Token:")} ${fmt.dim("[hidden]")}`);
227
- if (info.createdAt) {
228
- console.log(`${fmt.bold("Created:")} ${info.createdAt}`);
446
+ if (entry.apps && entry.apps.length > 0) {
447
+ console.log(`${fmt.bold("Apps:")} ${entry.apps.join(", ")}`);
448
+ }
449
+ if (info.createdAt || entry.createdAt) {
450
+ console.log(`${fmt.bold("Created:")} ${info.createdAt || entry.createdAt}`);
229
451
  }
230
452
  console.log("");
231
453
  }
232
454
 
233
455
  export async function dbShell(name, options) {
234
- name = resolveAppName(name);
235
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
236
- var dbCfg = getCloudCfg(dbCloud);
237
- var dbProvider = await getProvider(dbCloud, "db");
456
+ var resolved = resolveDatabase(name);
457
+ name = resolved.name;
458
+ var entry = resolved.entry;
238
459
 
239
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
460
+ var { provider, cfg } = await loadProvider(entry);
240
461
 
241
462
  // Verify database exists
242
463
  try {
243
- await dbProvider.getDatabaseInfo(dbCfg, name, { dbId });
464
+ await provider.getDatabaseInfo(cfg, name, {
465
+ dbId: entry.dbId,
466
+ connectionUrl: entry.connectionUrl,
467
+ });
244
468
  } catch (e) {
245
469
  fatal(e.message);
246
470
  }
247
471
 
472
+ var isPostgres = entry.isPostgres;
473
+
248
474
  var rl = createInterface({
249
475
  input: process.stdin,
250
476
  output: process.stderr,
251
477
  prompt: "sql> ",
252
478
  });
253
479
 
254
- process.stderr.write(`Connected to ${fmt.app(`relight-${name}`)}. Type .exit to quit.\n\n`);
480
+ process.stderr.write(`Connected to ${fmt.app(name)}. Type .exit to quit.\n\n`);
255
481
  rl.prompt();
256
482
 
257
483
  rl.on("line", async (line) => {
@@ -269,7 +495,7 @@ export async function dbShell(name, options) {
269
495
  try {
270
496
  var sql;
271
497
  if (line === ".tables") {
272
- if (dbCloud === "gcp" || dbCloud === "aws") {
498
+ if (isPostgres) {
273
499
  sql = "SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename";
274
500
  } else {
275
501
  sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%' ORDER BY name";
@@ -281,7 +507,7 @@ export async function dbShell(name, options) {
281
507
  rl.prompt();
282
508
  return;
283
509
  }
284
- if (dbCloud === "gcp" || dbCloud === "aws") {
510
+ if (isPostgres) {
285
511
  sql = `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '${tableName}' AND table_schema = 'public' ORDER BY ordinal_position`;
286
512
  } else {
287
513
  sql = `SELECT sql FROM sqlite_master WHERE name='${tableName}'`;
@@ -290,7 +516,10 @@ export async function dbShell(name, options) {
290
516
  sql = line;
291
517
  }
292
518
 
293
- var results = await dbProvider.queryDatabase(dbCfg, name, sql, undefined, { dbId });
519
+ var results = await provider.queryDatabase(cfg, name, sql, undefined, {
520
+ dbId: entry.dbId,
521
+ connectionUrl: entry.connectionUrl,
522
+ });
294
523
  var result = Array.isArray(results) ? results[0] : results;
295
524
 
296
525
  if (result && result.results && result.results.length > 0) {
@@ -329,16 +558,18 @@ export async function dbQuery(args, options) {
329
558
  sql = joined;
330
559
  }
331
560
 
332
- name = resolveAppName(name);
333
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
334
- var dbCfg = getCloudCfg(dbCloud);
335
- var dbProvider = await getProvider(dbCloud, "db");
561
+ var resolved = resolveDatabase(name);
562
+ name = resolved.name;
563
+ var entry = resolved.entry;
336
564
 
337
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
565
+ var { provider, cfg } = await loadProvider(entry);
338
566
 
339
567
  var results;
340
568
  try {
341
- results = await dbProvider.queryDatabase(dbCfg, name, sql, undefined, { dbId });
569
+ results = await provider.queryDatabase(cfg, name, sql, undefined, {
570
+ dbId: entry.dbId,
571
+ connectionUrl: entry.connectionUrl,
572
+ });
342
573
  } catch (e) {
343
574
  fatal(e.message);
344
575
  }
@@ -369,15 +600,14 @@ export async function dbImport(args, options) {
369
600
  } else if (args.length === 1) {
370
601
  filepath = args[0];
371
602
  } else {
372
- fatal("Usage: relight db import [name] <path>");
603
+ fatal("Usage: relight db import <name> <path>");
373
604
  }
374
605
 
375
- name = resolveAppName(name);
376
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
377
- var dbCfg = getCloudCfg(dbCloud);
378
- var dbProvider = await getProvider(dbCloud, "db");
606
+ var resolved = resolveDatabase(name);
607
+ name = resolved.name;
608
+ var entry = resolved.entry;
379
609
 
380
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
610
+ var { provider, cfg } = await loadProvider(entry);
381
611
 
382
612
  var sqlContent;
383
613
  try {
@@ -390,28 +620,33 @@ export async function dbImport(args, options) {
390
620
  status(`File: ${filepath} (${(sqlContent.length / 1024).toFixed(1)} KB)`);
391
621
 
392
622
  try {
393
- await dbProvider.importDatabase(dbCfg, name, sqlContent, { dbId });
623
+ await provider.importDatabase(cfg, name, sqlContent, {
624
+ dbId: entry.dbId,
625
+ connectionUrl: entry.connectionUrl,
626
+ });
394
627
  } catch (e) {
395
628
  fatal(e.message);
396
629
  }
397
630
 
398
- success(`Imported ${filepath} into ${fmt.app(`relight-${name}`)}`);
631
+ success(`Imported ${filepath} into ${fmt.app(name)}`);
399
632
  }
400
633
 
401
634
  export async function dbExport(name, options) {
402
- name = resolveAppName(name);
403
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
404
- var dbCfg = getCloudCfg(dbCloud);
405
- var dbProvider = await getProvider(dbCloud, "db");
635
+ var resolved = resolveDatabase(name);
636
+ name = resolved.name;
637
+ var entry = resolved.entry;
406
638
 
407
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
639
+ var { provider, cfg } = await loadProvider(entry);
408
640
 
409
641
  phase("Exporting database");
410
642
  status("Initiating export...");
411
643
 
412
644
  var dump;
413
645
  try {
414
- dump = await dbProvider.exportDatabase(dbCfg, name, { dbId });
646
+ dump = await provider.exportDatabase(cfg, name, {
647
+ dbId: entry.dbId,
648
+ connectionUrl: entry.connectionUrl,
649
+ });
415
650
  } catch (e) {
416
651
  fatal(e.message);
417
652
  }
@@ -425,47 +660,59 @@ export async function dbExport(name, options) {
425
660
  }
426
661
 
427
662
  export async function dbToken(name, options) {
428
- name = resolveAppName(name);
429
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
430
- var dbCfg = getCloudCfg(dbCloud);
431
- var dbProvider = await getProvider(dbCloud, "db");
663
+ var resolved = resolveDatabase(name);
664
+ name = resolved.name;
665
+ var entry = resolved.entry;
432
666
 
433
667
  if (options.rotate) {
434
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
668
+ var { provider, cfg } = await loadProvider(entry);
435
669
 
436
670
  var result;
437
671
  try {
438
- result = await dbProvider.rotateToken(dbCfg, name, {
439
- dbId,
440
- skipAppConfig: crossCloud,
441
- });
672
+ result = await provider.rotateToken(cfg, name, { dbId: entry.dbId });
442
673
  } catch (e) {
443
674
  fatal(e.message);
444
675
  }
445
676
 
446
- // Cross-cloud: update env vars on app cloud
447
- if (crossCloud) {
448
- var appCfg = getCloudCfg(appCloud);
449
- var appProvider = await getProvider(appCloud, "app");
450
- var appConfig = await appProvider.getAppConfig(appCfg, name);
451
-
452
- if (!appConfig.envKeys) appConfig.envKeys = [];
453
- if (!appConfig.secretKeys) appConfig.secretKeys = [];
454
- if (!appConfig.env) appConfig.env = {};
455
-
456
- appConfig.env["DB_TOKEN"] = "[hidden]";
457
- if (!appConfig.secretKeys.includes("DB_TOKEN")) appConfig.secretKeys.push("DB_TOKEN");
458
- appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
459
-
460
- if (result.connectionUrl) {
461
- var urlKey = dbCloud === "cf" ? "DB_URL" : "DATABASE_URL";
462
- appConfig.env[urlKey] = result.connectionUrl;
463
- if (!appConfig.envKeys.includes(urlKey)) appConfig.envKeys.push(urlKey);
677
+ // Update registry with new token and connection URL
678
+ entry.dbToken = result.dbToken;
679
+ if (result.connectionUrl) entry.connectionUrl = result.connectionUrl;
680
+ saveDatabaseConfig(name, entry);
681
+
682
+ // Update all attached apps
683
+ if (entry.apps && entry.apps.length > 0) {
684
+ for (var appName of entry.apps) {
685
+ status(`Updating ${appName}...`);
686
+ try {
687
+ // Re-attach to update the token in the app
688
+ var appCloud = resolveCloudId(null);
689
+ var appCfg = getCloudCfg(appCloud);
690
+ var appProvider = await getProvider(appCloud, "app");
691
+ var appConfig = await appProvider.getAppConfig(appCfg, appName);
692
+
693
+ if (appConfig) {
694
+ if (!appConfig.envKeys) appConfig.envKeys = [];
695
+ if (!appConfig.secretKeys) appConfig.secretKeys = [];
696
+ if (!appConfig.env) appConfig.env = {};
697
+
698
+ appConfig.env["DB_TOKEN"] = "[hidden]";
699
+ if (!appConfig.secretKeys.includes("DB_TOKEN")) appConfig.secretKeys.push("DB_TOKEN");
700
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
701
+
702
+ if (result.connectionUrl) {
703
+ var urlKey = entry.isPostgres ? "DATABASE_URL" : "DB_URL";
704
+ appConfig.env[urlKey] = result.connectionUrl;
705
+ if (!appConfig.envKeys.includes(urlKey)) appConfig.envKeys.push(urlKey);
706
+ }
707
+
708
+ await appProvider.pushAppConfig(appCfg, appName, appConfig, {
709
+ newSecrets: { DB_TOKEN: result.dbToken },
710
+ });
711
+ }
712
+ } catch (e) {
713
+ process.stderr.write(` ${fmt.dim(`Warning: could not update ${appName}: ${e.message}`)}\n`);
714
+ }
464
715
  }
465
-
466
- await appProvider.pushAppConfig(appCfg, name, appConfig, {
467
- newSecrets: { DB_TOKEN: result.dbToken },
468
- });
469
716
  }
470
717
 
471
718
  success("Token rotated.");
@@ -475,22 +722,16 @@ export async function dbToken(name, options) {
475
722
  }
476
723
  } else {
477
724
  console.log(`${fmt.bold("Token:")} ${fmt.dim("[hidden] - use --rotate to generate a new token")}`);
478
- // Try to show connection URL
479
- try {
480
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
481
- var info = await dbProvider.getDatabaseInfo(dbCfg, name, { dbId });
482
- if (info.connectionUrl) {
483
- console.log(`${fmt.bold("DB URL:")} ${fmt.url(info.connectionUrl)}`);
484
- }
485
- } catch {}
725
+ if (entry.connectionUrl) {
726
+ console.log(`${fmt.bold("DB URL:")} ${fmt.url(entry.connectionUrl)}`);
727
+ }
486
728
  }
487
729
  }
488
730
 
489
731
  export async function dbReset(name, options) {
490
- name = resolveAppName(name);
491
- var { appCloud, dbCloud, crossCloud } = resolveDbClouds(options);
492
- var dbCfg = getCloudCfg(dbCloud);
493
- var dbProvider = await getProvider(dbCloud, "db");
732
+ var resolved = resolveDatabase(name);
733
+ name = resolved.name;
734
+ var entry = resolved.entry;
494
735
 
495
736
  if (options.confirm !== name) {
496
737
  if (process.stdin.isTTY) {
@@ -510,14 +751,17 @@ export async function dbReset(name, options) {
510
751
  }
511
752
  }
512
753
 
513
- var dbId = crossCloud ? await getDbIdFromAppCloud(appCloud, name) : undefined;
754
+ var { provider, cfg } = await loadProvider(entry);
514
755
 
515
756
  phase("Resetting database");
516
757
  status("Listing tables...");
517
758
 
518
759
  var tables;
519
760
  try {
520
- tables = await dbProvider.resetDatabase(dbCfg, name, { dbId });
761
+ tables = await provider.resetDatabase(cfg, name, {
762
+ dbId: entry.dbId,
763
+ connectionUrl: entry.connectionUrl,
764
+ });
521
765
  } catch (e) {
522
766
  fatal(e.message);
523
767
  }