@motherbase/cli 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -18
  3. package/bin/motherbase.js +10 -10
  4. package/package.json +35 -26
  5. package/src/api.js +233 -229
  6. package/src/commands/_util.js +106 -50
  7. package/src/commands/backups.js +16 -0
  8. package/src/commands/branches.js +113 -0
  9. package/src/commands/completion.js +90 -0
  10. package/src/commands/config.js +47 -47
  11. package/src/commands/cron.js +65 -65
  12. package/src/commands/db-catalog.js +326 -0
  13. package/src/commands/db.js +103 -92
  14. package/src/commands/domains.js +124 -0
  15. package/src/commands/functions.js +63 -33
  16. package/src/commands/gen.js +63 -17
  17. package/src/commands/import.js +55 -35
  18. package/src/commands/index.js +85 -64
  19. package/src/commands/inspect.js +178 -41
  20. package/src/commands/link.js +35 -35
  21. package/src/commands/local.js +23 -2
  22. package/src/commands/login.js +39 -39
  23. package/src/commands/logout.js +16 -16
  24. package/src/commands/mcp.js +27 -9
  25. package/src/commands/migrate.js +207 -0
  26. package/src/commands/migration.js +108 -33
  27. package/src/commands/network.js +83 -0
  28. package/src/commands/orgs.js +40 -0
  29. package/src/commands/postgres-config.js +113 -0
  30. package/src/commands/projects.js +130 -125
  31. package/src/commands/queues.js +133 -0
  32. package/src/commands/realtime.js +35 -35
  33. package/src/commands/rls.js +176 -176
  34. package/src/commands/secrets.js +66 -45
  35. package/src/commands/signing.js +130 -0
  36. package/src/commands/snippets.js +44 -0
  37. package/src/commands/status.js +42 -42
  38. package/src/commands/storage.js +261 -113
  39. package/src/commands/test.js +9 -0
  40. package/src/commands/unlink.js +15 -15
  41. package/src/commands/users.js +142 -113
  42. package/src/commands/vault.js +82 -0
  43. package/src/commands/webhooks.js +62 -62
  44. package/src/commands/whoami.js +20 -20
  45. package/src/commands/wrappers.js +150 -0
  46. package/src/config.js +98 -98
  47. package/src/context.js +106 -102
  48. package/src/docs.js +68 -0
  49. package/src/edge-templates.js +194 -0
  50. package/src/format.js +92 -0
  51. package/src/inspect/csv.js +22 -0
  52. package/src/inspect/queries.js +562 -0
  53. package/src/main.js +224 -160
  54. package/src/mcp/platform-tools.js +233 -0
  55. package/src/mcp/protocol.js +4 -1
  56. package/src/mcp/storage-tools.js +93 -0
  57. package/src/mcp/tools.js +318 -6
  58. package/src/migration/commands.js +375 -0
  59. package/src/migration/files.js +245 -0
  60. package/src/prompt.js +91 -91
  61. package/src/storage/remote.js +62 -0
  62. package/src/ui.js +111 -111
  63. package/src/commands/migrations.js +0 -85
@@ -0,0 +1,326 @@
1
+ 'use strict';
2
+
3
+ // `motherbase db indexes|types|roles|publications`: the Database section of
4
+ // Studio from the terminal. Supabase's CLI has no equivalent for these, so they
5
+ // live under `db` rather than claiming top-level names Supabase might use.
6
+
7
+ const api = require('../api');
8
+ const { output, requireArg } = require('./_util');
9
+ const { CliError } = require('../context');
10
+
11
+ const schemaQuery = flags => (flags.schema ? { schema: flags.schema } : undefined);
12
+ const csv = value => String(value).split(',').map(s => s.trim()).filter(Boolean);
13
+
14
+ // `--login`, `--login true` and `--login false` all read naturally; absent
15
+ // means "leave as is", which ALTER ROLE needs to change one attribute only.
16
+ function bool(value) {
17
+ if (value === undefined) return undefined;
18
+ return value === true || value === 'true';
19
+ }
20
+
21
+ // Parse `schema.table` (or a bare `table` in public) into the API's shape.
22
+ function tableRef(ref) {
23
+ const [schema, table] = ref.includes('.') ? ref.split('.', 2) : ['public', ref];
24
+ return { schema, table };
25
+ }
26
+
27
+ function roleAttributes(flags) {
28
+ const attributes = {
29
+ canLogin: bool(flags.login),
30
+ isSuperuser: bool(flags.superuser),
31
+ canCreateDb: bool(flags.createdb),
32
+ canCreateRole: bool(flags.createrole),
33
+ canReplicate: bool(flags.replication),
34
+ canBypassRls: bool(flags.bypassrls),
35
+ inheritRole: bool(flags.inherit),
36
+ connectionLimit: flags['connection-limit'] === undefined ? undefined : Number(flags['connection-limit']),
37
+ password: typeof flags.password === 'string' ? flags.password : undefined,
38
+ validUntil: flags['valid-until'] === undefined ? undefined : String(flags['valid-until']),
39
+ };
40
+ return Object.fromEntries(Object.entries(attributes).filter(([, v]) => v !== undefined));
41
+ }
42
+
43
+ function publishFlags(flags, defaults) {
44
+ const actions = flags.publish === undefined ? null : csv(flags.publish);
45
+ if (!actions) return defaults;
46
+ return Object.fromEntries(['insert', 'update', 'delete', 'truncate'].map(a => [a, actions.includes(a)]));
47
+ }
48
+
49
+ const indexes = {
50
+ summary: 'Indexes across a schema: list, create, drop',
51
+ usage: 'motherbase db indexes <list|create|drop> ...',
52
+ subcommands: {
53
+ list: {
54
+ summary: 'List every index in a schema',
55
+ usage: 'motherbase db indexes list [--schema public]',
56
+ async run(ctx, args, flags) {
57
+ const data = await api.adminRequest(ctx, 'GET', '/schema/indexes', { query: schemaQuery(flags) });
58
+ output(ctx, data, (d, ui) =>
59
+ ui.table((d.indexes || []).map(i => ({ ...i, columns: i.columns.join(', ') })), [
60
+ { key: 'table', label: 'TABLE' },
61
+ { key: 'name', label: 'INDEX' },
62
+ { key: 'method', label: 'METHOD' },
63
+ { key: 'unique', label: 'UNIQUE' },
64
+ { key: 'columns', label: 'COLUMNS' },
65
+ { key: 'sizeBytes', label: 'BYTES' },
66
+ ]),
67
+ );
68
+ },
69
+ },
70
+ create: {
71
+ summary: 'Create an index on one or more columns',
72
+ usage: 'motherbase db indexes create <table> --columns a,b [--unique] [--method btree|hash|gist|gin|brin|spgist] [--name n] [--schema s]',
73
+ async run(ctx, args, flags) {
74
+ const table = requireArg(args[0], 'table');
75
+ if (!flags.columns) throw new CliError('--columns is required');
76
+ const body = { columns: csv(flags.columns), unique: bool(flags.unique) || false };
77
+ if (flags.method) body.method = String(flags.method);
78
+ if (flags.name) body.name = String(flags.name);
79
+ const data = await api.adminRequest(ctx, 'POST', `/schema/tables/${encodeURIComponent(table)}/indexes`, { body, query: schemaQuery(flags) });
80
+ output(ctx, data, (_d, ui) => ui.success(`Created index on ${table} (${body.columns.join(', ')})`));
81
+ },
82
+ },
83
+ drop: {
84
+ summary: 'Drop an index by name',
85
+ usage: 'motherbase db indexes drop <name> [--schema s]',
86
+ async run(ctx, args, flags) {
87
+ const name = requireArg(args[0], 'name');
88
+ const data = await api.adminRequest(ctx, 'DELETE', `/schema/indexes/${encodeURIComponent(name)}`, { query: schemaQuery(flags) });
89
+ output(ctx, data, (_d, ui) => ui.success(`Dropped index ${name}`));
90
+ },
91
+ },
92
+ },
93
+ };
94
+
95
+ const enumPath = name => `/schema/enums/${encodeURIComponent(name)}`;
96
+
97
+ const types = {
98
+ summary: 'Enumerated types: list, create, add-value, rename-value, drop',
99
+ usage: 'motherbase db types <list|create|add-value|rename-value|drop> ...',
100
+ subcommands: {
101
+ list: {
102
+ summary: 'List enum types and their values',
103
+ usage: 'motherbase db types list [--schema public]',
104
+ async run(ctx, args, flags) {
105
+ const data = await api.adminRequest(ctx, 'GET', '/schema/enums', { query: schemaQuery(flags) });
106
+ output(ctx, data, (d, ui) =>
107
+ ui.table((d.enums || []).map(e => ({ name: e.name, values: e.values.join(', '), usedBy: e.usedBy.length })), [
108
+ { key: 'name', label: 'TYPE' },
109
+ { key: 'values', label: 'VALUES' },
110
+ { key: 'usedBy', label: 'COLUMNS' },
111
+ ]),
112
+ );
113
+ },
114
+ },
115
+ create: {
116
+ summary: 'Create an enum type',
117
+ usage: 'motherbase db types create <name> --values a,b,c [--schema s]',
118
+ async run(ctx, args, flags) {
119
+ const name = requireArg(args[0], 'name');
120
+ if (!flags.values) throw new CliError('--values is required');
121
+ const data = await api.adminRequest(ctx, 'POST', '/schema/enums', { body: { name, values: csv(flags.values) }, query: schemaQuery(flags) });
122
+ output(ctx, data, (_d, ui) => ui.success(`Created type ${name}`));
123
+ },
124
+ },
125
+ 'add-value': {
126
+ summary: 'Add a value to an enum (values cannot be removed)',
127
+ usage: 'motherbase db types add-value <name> <value> [--before v | --after v] [--schema s]',
128
+ async run(ctx, args, flags) {
129
+ const name = requireArg(args[0], 'name');
130
+ const value = requireArg(args[1], 'value');
131
+ const body = { action: 'add-value', value, before: flags.before, after: flags.after };
132
+ const data = await api.adminRequest(ctx, 'PATCH', enumPath(name), { body, query: schemaQuery(flags) });
133
+ output(ctx, data, (_d, ui) => ui.success(`Added "${value}" to ${name}`));
134
+ },
135
+ },
136
+ 'rename-value': {
137
+ summary: 'Rename one value of an enum',
138
+ usage: 'motherbase db types rename-value <name> <from> <to> [--schema s]',
139
+ async run(ctx, args, flags) {
140
+ const name = requireArg(args[0], 'name');
141
+ const body = { action: 'rename-value', from: requireArg(args[1], 'from'), to: requireArg(args[2], 'to') };
142
+ const data = await api.adminRequest(ctx, 'PATCH', enumPath(name), { body, query: schemaQuery(flags) });
143
+ output(ctx, data, (_d, ui) => ui.success(`Renamed "${body.from}" to "${body.to}" in ${name}`));
144
+ },
145
+ },
146
+ drop: {
147
+ summary: 'Drop an enum type (refused while a column uses it)',
148
+ usage: 'motherbase db types drop <name> [--schema s]',
149
+ async run(ctx, args, flags) {
150
+ const name = requireArg(args[0], 'name');
151
+ const data = await api.adminRequest(ctx, 'DELETE', enumPath(name), { query: schemaQuery(flags) });
152
+ output(ctx, data, (_d, ui) => ui.success(`Dropped type ${name}`));
153
+ },
154
+ },
155
+ },
156
+ };
157
+
158
+ const ROLE_FLAGS = '[--login] [--password p] [--createdb] [--createrole] [--replication] [--bypassrls] [--superuser] [--connection-limit n] [--valid-until iso]';
159
+
160
+ const roles = {
161
+ summary: 'Postgres roles: list, create, alter, drop',
162
+ usage: 'motherbase db roles <list|create|alter|drop> ...',
163
+ subcommands: {
164
+ list: {
165
+ summary: 'List roles with their attributes and live connections',
166
+ usage: 'motherbase db roles list',
167
+ async run(ctx) {
168
+ const data = await api.adminRequest(ctx, 'GET', '/database/roles');
169
+ output(ctx, data, (d, ui) =>
170
+ ui.table(d.roles || [], [
171
+ { key: 'name', label: 'ROLE' },
172
+ { key: 'canLogin', label: 'LOGIN' },
173
+ { key: 'isSuperuser', label: 'SUPERUSER' },
174
+ { key: 'canBypassRls', label: 'BYPASSRLS' },
175
+ { key: 'connectionLimit', label: 'LIMIT' },
176
+ { key: 'activeConnections', label: 'CONNECTIONS' },
177
+ { key: 'reserved', label: 'RESERVED' },
178
+ ]),
179
+ );
180
+ },
181
+ },
182
+ create: {
183
+ summary: 'Create a role',
184
+ usage: `motherbase db roles create <name> ${ROLE_FLAGS}`,
185
+ async run(ctx, args, flags) {
186
+ const name = requireArg(args[0], 'name');
187
+ const data = await api.adminRequest(ctx, 'POST', '/database/roles', { body: { name, ...roleAttributes(flags) } });
188
+ output(ctx, data, (_d, ui) => ui.success(`Created role ${name}`));
189
+ },
190
+ },
191
+ alter: {
192
+ summary: 'Change attributes of a role, or rename it with --rename',
193
+ usage: `motherbase db roles alter <name> [--rename new] ${ROLE_FLAGS}`,
194
+ async run(ctx, args, flags) {
195
+ const name = requireArg(args[0], 'name');
196
+ const body = { ...roleAttributes(flags), ...(flags.rename ? { name: String(flags.rename) } : {}) };
197
+ if (!Object.keys(body).length) throw new CliError('Nothing to change: pass at least one attribute flag.');
198
+ const data = await api.adminRequest(ctx, 'PATCH', `/database/roles/${encodeURIComponent(name)}`, { body });
199
+ output(ctx, data, (_d, ui) => ui.success(`Updated role ${name}`));
200
+ },
201
+ },
202
+ drop: {
203
+ summary: 'Drop a role (refused for reserved roles and roles that own objects)',
204
+ usage: 'motherbase db roles drop <name>',
205
+ async run(ctx, args) {
206
+ const name = requireArg(args[0], 'name');
207
+ const data = await api.adminRequest(ctx, 'DELETE', `/database/roles/${encodeURIComponent(name)}`);
208
+ output(ctx, data, (_d, ui) => ui.success(`Dropped role ${name}`));
209
+ },
210
+ },
211
+ },
212
+ };
213
+
214
+ const publications = {
215
+ summary: 'Logical-replication publications: list, create, alter, drop',
216
+ usage: 'motherbase db publications <list|create|alter|drop> ...',
217
+ subcommands: {
218
+ list: {
219
+ summary: 'List publications, their tables and published actions',
220
+ usage: 'motherbase db publications list',
221
+ async run(ctx) {
222
+ const data = await api.adminRequest(ctx, 'GET', '/database/publications');
223
+ output(ctx, data, (d, ui) => {
224
+ ui.table((d.publications || []).map(p => ({
225
+ name: p.name,
226
+ publish: ['insert', 'update', 'delete', 'truncate'].filter(a => p[a]).join(', '),
227
+ tables: p.allTables ? '(all tables)' : p.tables.map(t => `${t.schema}.${t.table}`).join(', '),
228
+ })), [
229
+ { key: 'name', label: 'PUBLICATION' },
230
+ { key: 'publish', label: 'PUBLISH' },
231
+ { key: 'tables', label: 'TABLES' },
232
+ ]);
233
+ if (d.walLevel !== 'logical') ui.warn(`wal_level is "${d.walLevel}": publications stream nothing until it is "logical".`);
234
+ });
235
+ },
236
+ },
237
+ create: {
238
+ summary: 'Create a publication',
239
+ usage: 'motherbase db publications create <name> [--tables a,schema.b | --all-tables] [--publish insert,update,delete,truncate]',
240
+ async run(ctx, args, flags) {
241
+ const name = requireArg(args[0], 'name');
242
+ const body = {
243
+ name,
244
+ ...publishFlags(flags, { insert: true, update: true, delete: true, truncate: true }),
245
+ ...(flags['all-tables'] ? { allTables: true } : {}),
246
+ ...(flags.tables ? { tables: csv(flags.tables).map(tableRef) } : {}),
247
+ };
248
+ const data = await api.adminRequest(ctx, 'POST', '/database/publications', { body });
249
+ output(ctx, data, (_d, ui) => ui.success(`Created publication ${name}`));
250
+ },
251
+ },
252
+ alter: {
253
+ summary: 'Add or drop tables, or change what is published',
254
+ usage: 'motherbase db publications alter <name> [--add a,schema.b] [--drop c] [--publish insert,update]',
255
+ async run(ctx, args, flags) {
256
+ const name = requireArg(args[0], 'name');
257
+ const body = {
258
+ ...publishFlags(flags, {}),
259
+ ...(flags.add ? { addTables: csv(flags.add).map(tableRef) } : {}),
260
+ ...(flags.drop ? { dropTables: csv(flags.drop).map(tableRef) } : {}),
261
+ };
262
+ if (!Object.keys(body).length) throw new CliError('Nothing to change: pass --add, --drop or --publish.');
263
+ const data = await api.adminRequest(ctx, 'PATCH', `/database/publications/${encodeURIComponent(name)}`, { body });
264
+ output(ctx, data, (_d, ui) => ui.success(`Updated publication ${name}`));
265
+ },
266
+ },
267
+ drop: {
268
+ summary: 'Drop a publication',
269
+ usage: 'motherbase db publications drop <name>',
270
+ async run(ctx, args) {
271
+ const name = requireArg(args[0], 'name');
272
+ const data = await api.adminRequest(ctx, 'DELETE', `/database/publications/${encodeURIComponent(name)}`);
273
+ output(ctx, data, (_d, ui) => ui.success(`Dropped publication ${name}`));
274
+ },
275
+ },
276
+ },
277
+ };
278
+
279
+ function privilegeChange(grant) {
280
+ return {
281
+ summary: `${grant ? 'Grant' : 'Revoke'} a column privilege for a role`,
282
+ usage: `motherbase db column-privileges ${grant ? 'grant' : 'revoke'} <[schema.]table> --role r --privilege select|insert|update|references --columns a,b`,
283
+ async run(ctx, args, flags) {
284
+ const { schema, table } = tableRef(requireArg(args[0], 'table'));
285
+ if (!flags.role || !flags.privilege || !flags.columns) throw new CliError('--role, --privilege and --columns are required');
286
+ const body = {
287
+ schema,
288
+ table,
289
+ role: String(flags.role),
290
+ privilege: String(flags.privilege).toUpperCase(),
291
+ columns: csv(flags.columns),
292
+ grant,
293
+ };
294
+ const data = await api.adminRequest(ctx, 'PUT', '/database/column-privileges', { body });
295
+ output(ctx, data, (_d, ui) => ui.success(`${grant ? 'Granted' : 'Revoked'} ${body.privilege} on ${body.columns.join(', ')} ${grant ? 'to' : 'from'} ${body.role}`));
296
+ },
297
+ };
298
+ }
299
+
300
+ const columnPrivileges = {
301
+ summary: 'Column-level GRANT / REVOKE per role',
302
+ usage: 'motherbase db column-privileges <list|grant|revoke> <[schema.]table> ...',
303
+ subcommands: {
304
+ list: {
305
+ summary: 'Show which columns each role may select, insert, update or reference',
306
+ usage: 'motherbase db column-privileges list <[schema.]table>',
307
+ async run(ctx, args) {
308
+ const query = tableRef(requireArg(args[0], 'table'));
309
+ const data = await api.adminRequest(ctx, 'GET', '/database/column-privileges', { query });
310
+ output(ctx, data, (d, ui) =>
311
+ ui.table((d.roles || []).map(r => ({
312
+ role: r.role,
313
+ ...Object.fromEntries(Object.entries(r.privileges).map(([privilege, p]) => [
314
+ privilege,
315
+ p.tableLevel ? '(all)' : p.columns.join(', '),
316
+ ])),
317
+ })), ['role', 'SELECT', 'INSERT', 'UPDATE', 'REFERENCES']),
318
+ );
319
+ },
320
+ },
321
+ grant: privilegeChange(true),
322
+ revoke: privilegeChange(false),
323
+ },
324
+ };
325
+
326
+ module.exports = { indexes, types, roles, 'column-privileges': columnPrivileges, publications };
@@ -1,92 +1,103 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const api = require('../api');
6
- const prompt = require('../prompt');
7
- const { output, parseKeyValues } = require('./_util');
8
- const { CliError } = require('../context');
9
-
10
- const subcommands = {
11
- tables: {
12
- summary: 'List database tables',
13
- usage: 'motherbase db tables',
14
- async run(ctx) {
15
- const data = await api.adminRequest(ctx, 'GET', '/schema/tables');
16
- const tables = (data && data.tables) || data || [];
17
- output(ctx, data, (_d, ui) =>
18
- ui.table(Array.isArray(tables) ? tables : [], [
19
- { key: 'name', label: 'TABLE' },
20
- { key: 'schema', label: 'SCHEMA' },
21
- ]),
22
- );
23
- },
24
- },
25
- sql: {
26
- summary: 'Run SQL from -e "..." or a file',
27
- usage: 'motherbase db sql -e "select 1" | motherbase db sql <file.sql>',
28
- async run(ctx, args, flags) {
29
- let sql = flags.e || flags.execute;
30
- if (!sql && args[0]) sql = fs.readFileSync(path.resolve(ctx.cwd, args[0]), 'utf8');
31
- if (!sql) throw new CliError('Provide SQL via -e "..." or a file path.');
32
- const data = await api.adminRequest(ctx, 'POST', '/sql', { body: { sql } });
33
- output(ctx, data, (d, ui) => {
34
- if (d && Array.isArray(d.rows)) ui.table(d.rows);
35
- else if (Array.isArray(d)) ui.table(d);
36
- else ui.json(d);
37
- });
38
- },
39
- },
40
- dump: {
41
- summary: 'Create a database backup on the server',
42
- usage: 'motherbase db dump [file]',
43
- async run(ctx, args) {
44
- const body = args[0] ? { file: args[0] } : {};
45
- const data = await api.adminRequest(ctx, 'POST', '/backups', { body });
46
- output(ctx, data, (d, ui) => ui.success(`Backup created${d && d.file ? `: ${d.file}` : ''}`));
47
- },
48
- },
49
- restore: {
50
- summary: 'Restore the database from a server dump or offsite key',
51
- usage: 'motherbase db restore <file> | --key <remoteKey> --confirm',
52
- async run(ctx, args, flags) {
53
- const file = args[0];
54
- const key = flags.key;
55
- if (!file && !key) throw new CliError('Provide a <file> or --key <remoteKey>.');
56
- if (!flags.confirm && !flags.yes) {
57
- const ok = await prompt.confirm('This will overwrite the database. Continue?');
58
- if (!ok) throw new CliError('Aborted.');
59
- }
60
- const body = key ? { key, confirm: 'RESTORE' } : { file, confirm: 'RESTORE' };
61
- const data = await api.adminRequest(ctx, 'POST', '/backups/restore', { body });
62
- output(ctx, data, (d, ui) => ui.success('Restore complete'));
63
- },
64
- },
65
- backups: {
66
- summary: 'List backups, or manage backup config (`db backups config`)',
67
- usage: 'motherbase db backups [list] | motherbase db backups config [--set key=value ...]',
68
- async run(ctx, args, flags) {
69
- const sub = args[0];
70
- if (sub === 'config') {
71
- const sets = [].concat(flags.set || []).filter(Boolean);
72
- if (sets.length) {
73
- const body = parseKeyValues(sets);
74
- const data = await api.adminRequest(ctx, 'PUT', '/backups/config', { body });
75
- output(ctx, data, (d, ui) => ui.success('Backup config saved'));
76
- } else {
77
- const data = await api.adminRequest(ctx, 'GET', '/backups/config');
78
- output(ctx, data, (d, ui) => ui.keyValue(d || {}));
79
- }
80
- return;
81
- }
82
- const data = await api.adminRequest(ctx, 'GET', '/backups/list');
83
- output(ctx, data, (d, ui) => ui.json(d));
84
- },
85
- },
86
- };
87
-
88
- module.exports = {
89
- summary: 'Database: tables, SQL, dumps and restores',
90
- usage: 'motherbase db <tables|sql|dump|restore|backups> ...',
91
- subcommands,
92
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const api = require('../api');
6
+ const prompt = require('../prompt');
7
+ const rls = require('./rls');
8
+ const { output, alias, parseKeyValues } = require('./_util');
9
+ const { CliError } = require('../context');
10
+
11
+ const subcommands = {
12
+ tables: {
13
+ summary: 'List database tables',
14
+ usage: 'motherbase db tables',
15
+ async run(ctx) {
16
+ const data = await api.adminRequest(ctx, 'GET', '/schema/tables');
17
+ const tables = (data && data.tables) || data || [];
18
+ output(ctx, data, (_d, ui) =>
19
+ ui.table(Array.isArray(tables) ? tables : [], [
20
+ { key: 'name', label: 'TABLE' },
21
+ { key: 'schema', label: 'SCHEMA' },
22
+ ]),
23
+ );
24
+ },
25
+ },
26
+ sql: {
27
+ summary: 'Run SQL from -e "..." or a file',
28
+ usage: 'motherbase db sql -e "select 1" | motherbase db sql <file.sql>',
29
+ async run(ctx, args, flags) {
30
+ let sql = flags.e || flags.execute;
31
+ if (!sql && args[0]) sql = fs.readFileSync(path.resolve(ctx.cwd, args[0]), 'utf8');
32
+ if (!sql) throw new CliError('Provide SQL via -e "..." or a file path.');
33
+ const data = await api.adminRequest(ctx, 'POST', '/sql', { body: { sql } });
34
+ output(ctx, data, (d, ui) => {
35
+ if (d && Array.isArray(d.rows)) ui.table(d.rows);
36
+ else if (Array.isArray(d)) ui.table(d);
37
+ else ui.json(d);
38
+ });
39
+ },
40
+ },
41
+ // `db dump` is Supabase's local dump (../migration/commands); this is the
42
+ // server-side backup it used to be.
43
+ backup: {
44
+ summary: 'Create a database backup on the server',
45
+ usage: 'motherbase db backup [file]',
46
+ async run(ctx, args) {
47
+ const body = args[0] ? { file: args[0] } : {};
48
+ const data = await api.adminRequest(ctx, 'POST', '/backups', { body });
49
+ output(ctx, data, (d, ui) => ui.success(`Backup created${d && d.file ? `: ${d.file}` : ''}`));
50
+ },
51
+ },
52
+ restore: {
53
+ summary: 'Restore the database from a server dump or offsite key',
54
+ usage: 'motherbase db restore <file> | --key <remoteKey> --confirm',
55
+ async run(ctx, args, flags) {
56
+ const file = args[0];
57
+ const key = flags.key;
58
+ if (!file && !key) throw new CliError('Provide a <file> or --key <remoteKey>.');
59
+ if (!flags.confirm && !flags.yes) {
60
+ const ok = await prompt.confirm('This will overwrite the database. Continue?');
61
+ if (!ok) throw new CliError('Aborted.');
62
+ }
63
+ const body = key ? { key, confirm: 'RESTORE' } : { file, confirm: 'RESTORE' };
64
+ const data = await api.adminRequest(ctx, 'POST', '/backups/restore', { body });
65
+ output(ctx, data, (d, ui) => ui.success('Restore complete'));
66
+ },
67
+ },
68
+ backups: {
69
+ summary: 'List backups, or manage backup config (`db backups config`)',
70
+ usage: 'motherbase db backups [list] | motherbase db backups config [--set key=value ...]',
71
+ async run(ctx, args, flags) {
72
+ const sub = args[0];
73
+ if (sub === 'config') {
74
+ const sets = [].concat(flags.set || []).filter(Boolean);
75
+ if (sets.length) {
76
+ const body = parseKeyValues(sets);
77
+ const data = await api.adminRequest(ctx, 'PUT', '/backups/config', { body });
78
+ output(ctx, data, (d, ui) => ui.success('Backup config saved'));
79
+ } else {
80
+ const data = await api.adminRequest(ctx, 'GET', '/backups/config');
81
+ output(ctx, data, (d, ui) => ui.keyValue(d || {}));
82
+ }
83
+ return;
84
+ }
85
+ const data = await api.adminRequest(ctx, 'GET', '/backups/list');
86
+ output(ctx, data, (d, ui) => ui.json(d));
87
+ },
88
+ },
89
+ };
90
+
91
+ // Supabase's local-files workflow: push, pull, diff, reset, dump, seed.
92
+ Object.assign(subcommands, require('../migration/commands').dbSubcommands);
93
+ // Studio's Database section: indexes, enum types, roles, column privileges, publications.
94
+ Object.assign(subcommands, require('./db-catalog'));
95
+
96
+ // Supabase's `db lint`: the same advisor as `rls lint`.
97
+ subcommands.lint = alias(rls.subcommands.lint, 'motherbase db lint [--level critical|warning|info]');
98
+
99
+ module.exports = {
100
+ summary: 'Database: tables, SQL, dumps, restores, indexes, types, roles, publications',
101
+ usage: 'motherbase db <push|pull|diff|reset|dump|seed|tables|sql|backup|restore|backups|lint|indexes|types|roles|column-privileges|publications> ...',
102
+ subcommands,
103
+ };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ // Custom domains and vanity subdomains (Supabase's `supabase domains` and
4
+ // `supabase vanity-subdomains`). Both are Console routes on the linked project.
5
+
6
+ const api = require('../api');
7
+ const { output, requireArg, consoleProjectPath } = require('./_util');
8
+
9
+ function renderHostname(d, ui) {
10
+ const custom = d.customHostname;
11
+ if (!custom) {
12
+ ui.info(`No custom domain. The API answers on ${d.projectDomain}.`);
13
+ return;
14
+ }
15
+ ui.keyValue({ hostname: custom.hostname, status: custom.status, projectDomain: d.projectDomain });
16
+ if (custom.status !== 'active') {
17
+ ui.print('');
18
+ ui.print('Publish these DNS records, then run `motherbase domains reverify`:');
19
+ ui.table(custom.requiredRecords, [
20
+ { key: 'type', label: 'TYPE' },
21
+ { key: 'name', label: 'NAME' },
22
+ { key: 'value', label: 'VALUE' },
23
+ ]);
24
+ }
25
+ for (const check of custom.checks || []) {
26
+ ui[check.ok ? 'success' : 'warn'](`${check.record} ${check.name}: ${check.ok ? 'ok' : `found ${check.found.join(', ') || 'nothing'}`}`);
27
+ }
28
+ }
29
+
30
+ const hostnamePath = (ctx, suffix = '') => consoleProjectPath(ctx, `/custom-hostname${suffix}`);
31
+
32
+ const domains = {
33
+ summary: 'Custom domain for the linked project\'s API',
34
+ usage: 'motherbase domains <create|get|reverify|activate|delete> ...',
35
+ subcommands: {
36
+ create: {
37
+ summary: 'Start using a custom hostname (prints the DNS records to publish)',
38
+ usage: 'motherbase domains create --custom-hostname <api.example.com>',
39
+ async run(ctx, _args, flags) {
40
+ const hostname = requireArg(flags['custom-hostname'], 'custom-hostname');
41
+ const data = await api.consoleRequest(ctx, 'POST', hostnamePath(ctx), { body: { hostname } });
42
+ output(ctx, data, renderHostname);
43
+ },
44
+ },
45
+ get: {
46
+ summary: 'Show the custom hostname and its verification state',
47
+ usage: 'motherbase domains get',
48
+ async run(ctx) {
49
+ output(ctx, await api.consoleRequest(ctx, 'GET', hostnamePath(ctx)), renderHostname);
50
+ },
51
+ },
52
+ reverify: {
53
+ summary: 'Check the DNS records again',
54
+ usage: 'motherbase domains reverify',
55
+ async run(ctx) {
56
+ output(ctx, await api.consoleRequest(ctx, 'POST', hostnamePath(ctx, '/reverify')), renderHostname);
57
+ },
58
+ },
59
+ activate: {
60
+ summary: 'Serve the API on the verified hostname (TLS is issued on first request)',
61
+ usage: 'motherbase domains activate',
62
+ async run(ctx) {
63
+ output(ctx, await api.consoleRequest(ctx, 'POST', hostnamePath(ctx, '/activate')), renderHostname);
64
+ },
65
+ },
66
+ delete: {
67
+ summary: 'Stop using the custom hostname',
68
+ usage: 'motherbase domains delete',
69
+ async run(ctx) {
70
+ const data = await api.consoleRequest(ctx, 'DELETE', hostnamePath(ctx));
71
+ output(ctx, data, (_d, ui) => ui.success('Custom hostname removed'));
72
+ },
73
+ },
74
+ },
75
+ };
76
+
77
+ function renderVanity(d, ui) {
78
+ if (d.vanitySubdomain) ui.keyValue(d.vanitySubdomain);
79
+ else ui.info(`No vanity subdomain${d.vanitySuffix ? ` (they live under ${d.vanitySuffix})` : ''}.`);
80
+ }
81
+
82
+ const vanityPath = (ctx, suffix = '') => consoleProjectPath(ctx, `/vanity-subdomain${suffix}`);
83
+
84
+ const vanitySubdomains = {
85
+ summary: 'Vanity subdomain under the platform domain',
86
+ usage: 'motherbase vanity-subdomains <get|check-availability|activate|delete> ...',
87
+ subcommands: {
88
+ get: {
89
+ summary: 'Show the active vanity subdomain',
90
+ usage: 'motherbase vanity-subdomains get',
91
+ async run(ctx) {
92
+ output(ctx, await api.consoleRequest(ctx, 'GET', vanityPath(ctx)), renderVanity);
93
+ },
94
+ },
95
+ 'check-availability': {
96
+ summary: 'Is a subdomain free?',
97
+ usage: 'motherbase vanity-subdomains check-availability --desired-subdomain <name>',
98
+ async run(ctx, _args, flags) {
99
+ const subdomain = requireArg(flags['desired-subdomain'], 'desired-subdomain');
100
+ const data = await api.consoleRequest(ctx, 'POST', vanityPath(ctx, '/check-availability'), { body: { subdomain } });
101
+ output(ctx, data, (d, ui) => (d.available ? ui.success(`${d.hostname} is available`) : ui.warn(`${d.hostname} is taken`)));
102
+ },
103
+ },
104
+ activate: {
105
+ summary: 'Serve the API on <name>.<platform domain> as well',
106
+ usage: 'motherbase vanity-subdomains activate --desired-subdomain <name>',
107
+ async run(ctx, _args, flags) {
108
+ const subdomain = requireArg(flags['desired-subdomain'], 'desired-subdomain');
109
+ const data = await api.consoleRequest(ctx, 'POST', vanityPath(ctx), { body: { subdomain } });
110
+ output(ctx, data, renderVanity);
111
+ },
112
+ },
113
+ delete: {
114
+ summary: 'Remove the vanity subdomain',
115
+ usage: 'motherbase vanity-subdomains delete',
116
+ async run(ctx) {
117
+ const data = await api.consoleRequest(ctx, 'DELETE', vanityPath(ctx));
118
+ output(ctx, data, (_d, ui) => ui.success('Vanity subdomain removed'));
119
+ },
120
+ },
121
+ },
122
+ };
123
+
124
+ module.exports = { domains, vanitySubdomains };