@motherbase/cli 0.2.3 → 0.2.5
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/README.md +247 -134
- package/package.json +4 -3
- package/src/api.js +14 -2
- package/src/commands/_util.js +49 -6
- package/src/commands/backups.js +16 -0
- package/src/commands/branches.js +113 -0
- package/src/commands/completion.js +90 -0
- package/src/commands/db-catalog.js +326 -0
- package/src/commands/db.js +16 -5
- package/src/commands/domains.js +124 -0
- package/src/commands/functions.js +297 -274
- package/src/commands/gen.js +172 -126
- package/src/commands/import.js +23 -3
- package/src/commands/index.js +32 -6
- package/src/commands/inspect.js +142 -5
- package/src/commands/keys.js +72 -0
- package/src/commands/local.js +226 -17
- package/src/commands/mcp.js +61 -0
- package/src/commands/migrate.js +179 -0
- package/src/commands/migration.js +81 -6
- package/src/commands/network.js +83 -0
- package/src/commands/orgs.js +40 -0
- package/src/commands/postgres-config.js +113 -0
- package/src/commands/projects.js +8 -3
- package/src/commands/queues.js +133 -0
- package/src/commands/secrets.js +29 -8
- package/src/commands/signing.js +130 -0
- package/src/commands/snippets.js +44 -0
- package/src/commands/storage.js +192 -44
- package/src/commands/test.js +9 -0
- package/src/commands/users.js +30 -1
- package/src/commands/vault.js +82 -0
- package/src/commands/wrappers.js +150 -0
- package/src/edge-templates.js +194 -0
- package/src/format.js +92 -0
- package/src/inspect/csv.js +22 -0
- package/src/inspect/queries.js +562 -0
- package/src/main.js +41 -7
- package/src/mcp/platform-tools.js +233 -0
- package/src/mcp/protocol.js +149 -0
- package/src/mcp/storage-tools.js +93 -0
- package/src/mcp/tools.js +478 -0
- package/src/migration/commands.js +375 -0
- package/src/migration/files.js +245 -0
- package/src/prompt.js +1 -1
- package/src/storage/remote.js +62 -0
- package/src/commands/migrations.js +0 -85
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Preview branches (Supabase's `supabase branches`). A branch is a project of
|
|
4
|
+
// its own on the Console, cut from the linked project; these commands address
|
|
5
|
+
// it by its name (e.g. the git branch) or its project id.
|
|
6
|
+
|
|
7
|
+
const api = require('../api');
|
|
8
|
+
const config = require('../config');
|
|
9
|
+
const { output, requireArg, consoleProjectPath } = require('./_util');
|
|
10
|
+
|
|
11
|
+
const COLUMNS = [
|
|
12
|
+
{ key: 'name', label: 'NAME' },
|
|
13
|
+
{ key: 'project', label: 'PROJECT' },
|
|
14
|
+
{ key: 'gitBranch', label: 'GIT BRANCH' },
|
|
15
|
+
{ key: 'status', label: 'STATUS' },
|
|
16
|
+
{ key: 'running', label: 'RUNNING' },
|
|
17
|
+
{ key: 'createdAt', label: 'CREATED' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const branchPath = (ctx, name, action = '') =>
|
|
21
|
+
consoleProjectPath(ctx, `/branches/${encodeURIComponent(requireArg(name, 'name'))}${action}`);
|
|
22
|
+
|
|
23
|
+
function action(verb, summary, render) {
|
|
24
|
+
return {
|
|
25
|
+
summary,
|
|
26
|
+
usage: `motherbase branches ${verb} <name>`,
|
|
27
|
+
async run(ctx, args) {
|
|
28
|
+
const data = await api.consoleRequest(ctx, 'POST', branchPath(ctx, args[0], `/${verb}`));
|
|
29
|
+
output(ctx, data, (d, ui) => render(d, ui));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const subcommands = {
|
|
35
|
+
create: {
|
|
36
|
+
summary: 'Create a preview branch of the linked project',
|
|
37
|
+
usage: 'motherbase branches create <name> [--git-branch <ref>] [--with-data]',
|
|
38
|
+
async run(ctx, args, flags) {
|
|
39
|
+
const body = { name: requireArg(args[0], 'name'), withData: Boolean(flags['with-data']) };
|
|
40
|
+
if (flags['git-branch']) body.gitBranch = String(flags['git-branch']);
|
|
41
|
+
const data = await api.consoleRequest(ctx, 'POST', consoleProjectPath(ctx, '/branches'), { body });
|
|
42
|
+
output(ctx, data, (d, ui) => {
|
|
43
|
+
ui.success(`Creating branch ${d.name} as project ${d.project} (${d.domain})`);
|
|
44
|
+
ui.info('This takes a few minutes. Follow it with `motherbase branches get ' + d.name + '`.');
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
list: {
|
|
49
|
+
summary: 'List the branches of the linked project, main first',
|
|
50
|
+
usage: 'motherbase branches list',
|
|
51
|
+
async run(ctx) {
|
|
52
|
+
const rows = await api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches'));
|
|
53
|
+
output(ctx, { branches: rows }, (d, ui) => ui.table(d.branches, COLUMNS));
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
get: {
|
|
57
|
+
summary: 'Show one branch, with the migrations it is ahead and behind',
|
|
58
|
+
usage: 'motherbase branches get <name>',
|
|
59
|
+
async run(ctx, args) {
|
|
60
|
+
const data = await api.consoleRequest(ctx, 'GET', branchPath(ctx, args[0]));
|
|
61
|
+
output(ctx, data, (d, ui) => {
|
|
62
|
+
const { migrations, ...rest } = d;
|
|
63
|
+
ui.keyValue(rest);
|
|
64
|
+
if (migrations) ui.keyValue({ ahead: migrations.ahead.join(', ') || '-', behind: migrations.behind.join(', ') || '-' });
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
update: {
|
|
69
|
+
summary: 'Change a branch\'s git branch',
|
|
70
|
+
usage: 'motherbase branches update <name> --git-branch <ref>',
|
|
71
|
+
async run(ctx, args, flags) {
|
|
72
|
+
const body = {};
|
|
73
|
+
if (flags['git-branch'] !== undefined) body.gitBranch = String(flags['git-branch']);
|
|
74
|
+
const data = await api.consoleRequest(ctx, 'PATCH', branchPath(ctx, args[0]), { body });
|
|
75
|
+
output(ctx, data, (d, ui) => ui.success(`Updated branch ${d.name}`));
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
delete: {
|
|
79
|
+
summary: 'Delete a branch and its whole stack',
|
|
80
|
+
usage: 'motherbase branches delete <name>',
|
|
81
|
+
async run(ctx, args) {
|
|
82
|
+
const data = await api.consoleRequest(ctx, 'DELETE', branchPath(ctx, args[0]));
|
|
83
|
+
output(ctx, data, (d, ui) => ui.success(`Deleted branch ${d.name}`));
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
pause: action('pause', 'Stop a branch\'s stack, keeping its data', (d, ui) => ui.success(`Paused ${d.name}`)),
|
|
87
|
+
unpause: action('unpause', 'Start a paused branch again', (d, ui) => ui.success(`Unpaused ${d.name}`)),
|
|
88
|
+
reset: action('reset', 'Recreate a branch from the parent as it is now (its changes are lost)',
|
|
89
|
+
(d, ui) => ui.success(`Resetting ${d.name}; it is being cut again from ${d.parent}`)),
|
|
90
|
+
merge: action('merge', 'Apply the branch\'s new migrations to the parent',
|
|
91
|
+
(d, ui) => ui.success(d.applied.length ? `Merged ${d.applied.length} migration(s): ${d.applied.join(', ')}` : 'Nothing to merge')),
|
|
92
|
+
rebase: action('rebase', 'Apply the parent\'s newer migrations to the branch',
|
|
93
|
+
(d, ui) => ui.success(d.applied.length ? `Rebased ${d.applied.length} migration(s): ${d.applied.join(', ')}` : 'Already up to date')),
|
|
94
|
+
switch: {
|
|
95
|
+
summary: 'Point this directory\'s link at a branch (or back at main)',
|
|
96
|
+
usage: 'motherbase branches switch <name|main>',
|
|
97
|
+
async run(ctx, args) {
|
|
98
|
+
const name = requireArg(args[0], 'name');
|
|
99
|
+
const rows = await api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches'));
|
|
100
|
+
const match = rows.find(b => b.name === name || b.project === name || (name === 'main' && b.isDefault));
|
|
101
|
+
if (!match) throw new Error(`No branch ${name}. See \`motherbase branches list\`.`);
|
|
102
|
+
const link = config.readLink(ctx.cwd) || {};
|
|
103
|
+
config.writeLink(ctx.cwd, { ...link, consoleUrl: ctx.consoleUrl, projectId: match.project, projectName: match.project });
|
|
104
|
+
output(ctx, { project: match.project, branch: match.name }, (d, ui) => ui.success(`Linked ${ctx.cwd} → ${d.project} (${d.branch})`));
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
summary: 'Manage preview branches of the linked project',
|
|
111
|
+
usage: 'motherbase branches <create|list|get|update|delete|pause|unpause|reset|merge|rebase|switch> ...',
|
|
112
|
+
subcommands,
|
|
113
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `completion <shell>`: a completion script generated from the command
|
|
4
|
+
// registry, so it can never list a command that does not exist.
|
|
5
|
+
|
|
6
|
+
const { requireArg } = require('./_util');
|
|
7
|
+
const { CliError } = require('../context');
|
|
8
|
+
|
|
9
|
+
// Every command path that has children, with those children:
|
|
10
|
+
// [{ path: 'storage buckets', children: [{ name: 'list', summary }] }, ...].
|
|
11
|
+
// The root is the path ''.
|
|
12
|
+
function commandPaths(registry) {
|
|
13
|
+
const out = [];
|
|
14
|
+
const walk = (path, subcommands) => {
|
|
15
|
+
const names = Object.keys(subcommands);
|
|
16
|
+
out.push({ path, children: names.map(name => ({ name, summary: subcommands[name].summary || '' })) });
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const sub = subcommands[name].subcommands;
|
|
19
|
+
if (sub) walk(path ? `${path} ${name}` : name, sub);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
walk('', registry);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The words typed so far, flags skipped, are the path whose children complete.
|
|
27
|
+
function bash(paths) {
|
|
28
|
+
const cases = paths
|
|
29
|
+
.map(p => ` "${p.path}") words="${p.children.map(c => c.name).join(' ')}" ;;`)
|
|
30
|
+
.join('\n');
|
|
31
|
+
return `# motherbase bash completion. Load with: source <(motherbase completion bash)
|
|
32
|
+
_motherbase() {
|
|
33
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}" path="" words="" i
|
|
34
|
+
for ((i = 1; i < COMP_CWORD; i++)); do
|
|
35
|
+
[[ \${COMP_WORDS[i]} == -* ]] && continue
|
|
36
|
+
path="\${path:+$path }\${COMP_WORDS[i]}"
|
|
37
|
+
done
|
|
38
|
+
case "$path" in
|
|
39
|
+
${cases}
|
|
40
|
+
esac
|
|
41
|
+
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
42
|
+
}
|
|
43
|
+
complete -F _motherbase motherbase
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// zsh runs the bash function through its bashcompinit shim.
|
|
48
|
+
function zsh(paths) {
|
|
49
|
+
return `# motherbase zsh completion. Load with: source <(motherbase completion zsh)
|
|
50
|
+
autoload -U +X bashcompinit && bashcompinit
|
|
51
|
+
${bash(paths).split('\n').slice(1).join('\n')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fishQuote(text) {
|
|
55
|
+
return `'${String(text).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fish(paths) {
|
|
59
|
+
const lines = paths.flatMap(p =>
|
|
60
|
+
p.children.map(
|
|
61
|
+
c => `complete -c motherbase -f -n ${fishQuote(`__motherbase_at '${p.path}'`)} -a ${fishQuote(c.name)} -d ${fishQuote(c.summary)}`,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
return `# motherbase fish completion. Load with: motherbase completion fish | source
|
|
65
|
+
function __motherbase_at
|
|
66
|
+
set -l path (string join ' ' -- (string match -v -- '-*' (commandline -opc)[2..-1]))
|
|
67
|
+
test "$path" = "$argv[1]"
|
|
68
|
+
end
|
|
69
|
+
${lines.join('\n')}
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SHELLS = { bash, zsh, fish };
|
|
74
|
+
|
|
75
|
+
function completionScript(shell, registry) {
|
|
76
|
+
if (!SHELLS[shell]) throw new CliError(`shell must be one of: ${Object.keys(SHELLS).join(', ')}`);
|
|
77
|
+
return SHELLS[shell](commandPaths(registry));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
summary: 'Generate the autocompletion script for a shell',
|
|
82
|
+
usage: 'motherbase completion <bash|zsh|fish>',
|
|
83
|
+
run(ctx, args) {
|
|
84
|
+
const shell = requireArg(args[0], 'shell');
|
|
85
|
+
// Required here, not at the top: the registry itself requires this file.
|
|
86
|
+
const { registry } = require('./index');
|
|
87
|
+
process.stdout.write(completionScript(shell, registry));
|
|
88
|
+
},
|
|
89
|
+
completionScript,
|
|
90
|
+
};
|
|
@@ -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: flags.password === undefined ? undefined : String(flags.password),
|
|
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 };
|
package/src/commands/db.js
CHANGED
|
@@ -4,7 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const api = require('../api');
|
|
6
6
|
const prompt = require('../prompt');
|
|
7
|
-
const
|
|
7
|
+
const rls = require('./rls');
|
|
8
|
+
const { output, alias, parseKeyValues } = require('./_util');
|
|
8
9
|
const { CliError } = require('../context');
|
|
9
10
|
|
|
10
11
|
const subcommands = {
|
|
@@ -37,9 +38,11 @@ const subcommands = {
|
|
|
37
38
|
});
|
|
38
39
|
},
|
|
39
40
|
},
|
|
40
|
-
dump
|
|
41
|
+
// `db dump` is Supabase's local dump (../migration/commands); this is the
|
|
42
|
+
// server-side backup it used to be.
|
|
43
|
+
backup: {
|
|
41
44
|
summary: 'Create a database backup on the server',
|
|
42
|
-
usage: 'motherbase db
|
|
45
|
+
usage: 'motherbase db backup [file]',
|
|
43
46
|
async run(ctx, args) {
|
|
44
47
|
const body = args[0] ? { file: args[0] } : {};
|
|
45
48
|
const data = await api.adminRequest(ctx, 'POST', '/backups', { body });
|
|
@@ -85,8 +88,16 @@ const subcommands = {
|
|
|
85
88
|
},
|
|
86
89
|
};
|
|
87
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
|
+
|
|
88
99
|
module.exports = {
|
|
89
|
-
summary: 'Database: tables, SQL, dumps
|
|
90
|
-
usage: 'motherbase db <tables|sql|
|
|
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> ...',
|
|
91
102
|
subcommands,
|
|
92
103
|
};
|