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.
- package/package.json +1 -1
- package/src/cli.js +94 -49
- package/src/commands/apps.js +10 -10
- package/src/commands/auth.js +0 -62
- package/src/commands/config.js +16 -16
- package/src/commands/cost.js +6 -6
- package/src/commands/db.js +434 -190
- package/src/commands/deploy.js +38 -72
- package/src/commands/doctor.js +28 -4
- package/src/commands/domains.js +22 -22
- package/src/commands/logs.js +4 -4
- package/src/commands/open.js +4 -4
- package/src/commands/ps.js +4 -4
- package/src/commands/scale.js +4 -4
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/gcp.js +21 -1
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/config.js +169 -11
- package/src/lib/link.js +13 -2
- package/src/lib/providers/aws/db.js +217 -226
- package/src/lib/providers/cf/db.js +33 -131
- package/src/lib/providers/gcp/db.js +169 -254
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +33 -3
package/src/commands/db.js
CHANGED
|
@@ -1,84 +1,126 @@
|
|
|
1
1
|
import { phase, status, success, fatal, hint, fmt, table } from "../lib/output.js";
|
|
2
|
-
import { resolveAppName,
|
|
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
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
62
|
+
// Check if already exists
|
|
63
|
+
if (getDatabaseConfig(name)) {
|
|
64
|
+
fatal(`Database '${name}' already exists.`);
|
|
65
|
+
}
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
54
|
-
if (!appConfig.env) appConfig.env = {};
|
|
67
|
+
var providerId = resolveProvider(options);
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
97
|
+
var result;
|
|
98
|
+
try {
|
|
99
|
+
result = await provider.createDatabase(cfg, name, {
|
|
100
|
+
location: options.location,
|
|
101
|
+
jurisdiction: options.jurisdiction,
|
|
70
102
|
});
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
187
|
+
removeDatabaseConfig(name);
|
|
188
|
+
success(`Database ${fmt.app(name)} destroyed.`);
|
|
189
|
+
}
|
|
139
190
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
status(`Cleaning up app config on ${appCloud}...`);
|
|
149
|
-
delete appConfig.dbId;
|
|
150
|
-
delete appConfig.dbName;
|
|
221
|
+
appName = resolveAppName(appName);
|
|
151
222
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
if (appConfig.secretKeys) appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
|
|
240
|
+
}
|
|
159
241
|
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
175
|
-
async function
|
|
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
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
var
|
|
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
|
|
403
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
192
404
|
|
|
193
405
|
var info;
|
|
194
406
|
try {
|
|
195
|
-
info = await
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
228
|
-
console.log(`${fmt.bold("
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
var
|
|
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
|
|
460
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
240
461
|
|
|
241
462
|
// Verify database exists
|
|
242
463
|
try {
|
|
243
|
-
await
|
|
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(
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
var
|
|
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
|
|
565
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
338
566
|
|
|
339
567
|
var results;
|
|
340
568
|
try {
|
|
341
|
-
results = await
|
|
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
|
|
603
|
+
fatal("Usage: relight db import <name> <path>");
|
|
373
604
|
}
|
|
374
605
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
var
|
|
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
|
|
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
|
|
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(
|
|
631
|
+
success(`Imported ${filepath} into ${fmt.app(name)}`);
|
|
399
632
|
}
|
|
400
633
|
|
|
401
634
|
export async function dbExport(name, options) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
var
|
|
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
|
|
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
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
var
|
|
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
|
|
668
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
435
669
|
|
|
436
670
|
var result;
|
|
437
671
|
try {
|
|
438
|
-
result = await
|
|
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
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
var
|
|
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
|
|
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
|
|
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
|
}
|