@coldge.com/gitbase 1.0.2 → 1.0.3
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 +65 -15
- package/dist/api/supabase.js +139 -32
- package/dist/commands/branch.js +86 -14
- package/dist/commands/diff.js +180 -94
- package/dist/commands/init.js +8 -3
- package/dist/commands/log.js +85 -11
- package/dist/commands/merge.js +226 -15
- package/dist/commands/pull.js +171 -0
- package/dist/commands/push.js +324 -71
- package/dist/commands/revert.js +247 -95
- package/dist/commands/snapshot.js +49 -0
- package/dist/commands/stash.js +211 -0
- package/dist/commands/status.js +46 -38
- package/dist/commands/verify.js +120 -0
- package/dist/index.js +90 -10
- package/dist/schema/extractor.js +183 -26
- package/dist/schema/queries.js +160 -8
- package/dist/utils/hashing.js +52 -3
- package/dist/utils/sqlDiff.js +245 -0
- package/package.json +2 -1
package/dist/schema/extractor.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
import { runQuery } from '../api/supabase.js';
|
|
2
2
|
import * as Queries from './queries.js';
|
|
3
3
|
export async function extractSchema(projectRef) {
|
|
4
|
-
|
|
5
|
-
const functions = await
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// All extractors run in parallel for maximum performance
|
|
5
|
+
const [extensions, types, sequences, tables, matviews, views, functions, triggers, policies, grants, publications] = await Promise.all([
|
|
6
|
+
extractExtensions(projectRef),
|
|
7
|
+
extractTypes(projectRef),
|
|
8
|
+
extractSequences(projectRef),
|
|
9
|
+
extractTables(projectRef),
|
|
10
|
+
extractMaterializedViews(projectRef),
|
|
11
|
+
extractViews(projectRef),
|
|
12
|
+
extractFunctions(projectRef),
|
|
13
|
+
extractTriggers(projectRef),
|
|
14
|
+
extractPolicies(projectRef),
|
|
15
|
+
extractGrants(projectRef),
|
|
16
|
+
extractPublications(projectRef),
|
|
17
|
+
]);
|
|
18
|
+
return {
|
|
19
|
+
extensions, types, sequences, tables, matviews,
|
|
20
|
+
views, functions, triggers, policies, grants, publications
|
|
21
|
+
};
|
|
11
22
|
}
|
|
12
23
|
async function extractTables(projectRef) {
|
|
13
24
|
const tableList = await runQuery(projectRef, Queries.LIST_TABLES);
|
|
14
25
|
if (!Array.isArray(tableList))
|
|
15
26
|
return {};
|
|
16
27
|
const tables = {};
|
|
17
|
-
|
|
28
|
+
// Process all tables concurrently instead of sequentially
|
|
29
|
+
await Promise.all(tableList.map(async (row) => {
|
|
18
30
|
const tableName = row.table_name;
|
|
19
31
|
const rlsEnabled = row.rls_enabled;
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
// Fetch columns, constraints, and indexes in parallel per table
|
|
33
|
+
const [columns, constraints, indexes] = await Promise.all([
|
|
34
|
+
runQuery(projectRef, Queries.GET_COLUMNS(tableName)),
|
|
35
|
+
runQuery(projectRef, Queries.GET_TABLE_CONSTRAINTS(tableName)),
|
|
36
|
+
runQuery(projectRef, Queries.GET_TABLE_INDEXES(tableName)),
|
|
37
|
+
]);
|
|
38
|
+
// Column definitions
|
|
22
39
|
const colDefs = columns.map(c => {
|
|
23
40
|
let line = ` "${c.column_name}" ${c.data_type}`;
|
|
24
41
|
if (c.is_nullable === 'NO')
|
|
@@ -27,13 +44,23 @@ async function extractTables(projectRef) {
|
|
|
27
44
|
line += ` DEFAULT ${c.column_default}`;
|
|
28
45
|
return line;
|
|
29
46
|
});
|
|
30
|
-
|
|
47
|
+
// Constraint definitions (PK, FK, UNIQUE, CHECK)
|
|
48
|
+
const constraintDefs = constraints.map(c => {
|
|
49
|
+
return ` CONSTRAINT "${c.name}" ${c.definition}`;
|
|
50
|
+
});
|
|
51
|
+
const allDefs = [...colDefs, ...constraintDefs];
|
|
52
|
+
let sql = `CREATE TABLE public."${tableName}" (\n`;
|
|
53
|
+
sql += allDefs.join(',\n');
|
|
31
54
|
sql += '\n);';
|
|
55
|
+
// Standalone index statements (non-primary, appended after CREATE TABLE)
|
|
56
|
+
for (const idx of indexes) {
|
|
57
|
+
sql += `\n${idx.definition};`;
|
|
58
|
+
}
|
|
32
59
|
if (rlsEnabled) {
|
|
33
60
|
sql += `\nALTER TABLE public."${tableName}" ENABLE ROW LEVEL SECURITY;`;
|
|
34
61
|
}
|
|
35
62
|
tables[tableName] = sql;
|
|
36
|
-
}
|
|
63
|
+
}));
|
|
37
64
|
return tables;
|
|
38
65
|
}
|
|
39
66
|
async function extractFunctions(projectRef) {
|
|
@@ -56,6 +83,16 @@ async function extractViews(projectRef) {
|
|
|
56
83
|
}
|
|
57
84
|
return result;
|
|
58
85
|
}
|
|
86
|
+
async function extractMaterializedViews(projectRef) {
|
|
87
|
+
const matviews = await runQuery(projectRef, Queries.LIST_MAT_VIEWS);
|
|
88
|
+
if (!Array.isArray(matviews))
|
|
89
|
+
return {};
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const v of matviews) {
|
|
92
|
+
result[v.name] = `CREATE MATERIALIZED VIEW public."${v.name}" AS\n${v.definition}`;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
59
96
|
async function extractTriggers(projectRef) {
|
|
60
97
|
const triggers = await runQuery(projectRef, Queries.LIST_TRIGGERS);
|
|
61
98
|
if (!Array.isArray(triggers))
|
|
@@ -73,7 +110,6 @@ async function extractPolicies(projectRef) {
|
|
|
73
110
|
const result = {};
|
|
74
111
|
for (const p of policies) {
|
|
75
112
|
let sql = `CREATE POLICY "${p.name}"\nON public."${p.tablename}"\nFOR ${p.cmd}`;
|
|
76
|
-
// Handle roles
|
|
77
113
|
let roles = p.roles;
|
|
78
114
|
if (Array.isArray(roles)) {
|
|
79
115
|
roles = roles.join(', ');
|
|
@@ -81,9 +117,8 @@ async function extractPolicies(projectRef) {
|
|
|
81
117
|
else if (typeof roles === 'string' && roles.startsWith('{')) {
|
|
82
118
|
roles = roles.replace(/^\{|\}$/g, '').split(',').join(', ');
|
|
83
119
|
}
|
|
84
|
-
if (roles)
|
|
120
|
+
if (roles)
|
|
85
121
|
sql += `\nTO ${roles}`;
|
|
86
|
-
}
|
|
87
122
|
if (p.qual)
|
|
88
123
|
sql += `\nUSING (${p.qual})`;
|
|
89
124
|
if (p.with_check)
|
|
@@ -94,20 +129,142 @@ async function extractPolicies(projectRef) {
|
|
|
94
129
|
return result;
|
|
95
130
|
}
|
|
96
131
|
async function extractTypes(projectRef) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
const [enums, composites, domains] = await Promise.all([
|
|
133
|
+
runQuery(projectRef, Queries.LIST_ENUM_TYPES),
|
|
134
|
+
runQuery(projectRef, Queries.LIST_COMPOSITE_TYPES),
|
|
135
|
+
runQuery(projectRef, Queries.LIST_DOMAIN_TYPES),
|
|
136
|
+
]);
|
|
100
137
|
const result = {};
|
|
101
|
-
//
|
|
102
|
-
const
|
|
103
|
-
for (const t of
|
|
104
|
-
if (!
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
for (const [name, labels] of Object.entries(
|
|
138
|
+
// Enum types
|
|
139
|
+
const enumGrouped = {};
|
|
140
|
+
for (const t of enums) {
|
|
141
|
+
if (!enumGrouped[t.name])
|
|
142
|
+
enumGrouped[t.name] = [];
|
|
143
|
+
enumGrouped[t.name].push(t.label);
|
|
144
|
+
}
|
|
145
|
+
for (const [name, labels] of Object.entries(enumGrouped)) {
|
|
109
146
|
const labelsStr = labels.map(l => `'${l}'`).join(', ');
|
|
110
147
|
result[name] = `CREATE TYPE public."${name}" AS ENUM (${labelsStr});`;
|
|
111
148
|
}
|
|
149
|
+
// Composite types
|
|
150
|
+
const compGrouped = {};
|
|
151
|
+
for (const c of composites) {
|
|
152
|
+
if (!compGrouped[c.name])
|
|
153
|
+
compGrouped[c.name] = [];
|
|
154
|
+
compGrouped[c.name].push({ col_name: c.col_name, col_type: c.col_type });
|
|
155
|
+
}
|
|
156
|
+
for (const [name, cols] of Object.entries(compGrouped)) {
|
|
157
|
+
const colDefs = cols.map(c => ` "${c.col_name}" ${c.col_type}`).join(',\n');
|
|
158
|
+
result[name] = `CREATE TYPE public."${name}" AS (\n${colDefs}\n);`;
|
|
159
|
+
}
|
|
160
|
+
// Domain types
|
|
161
|
+
for (const d of domains) {
|
|
162
|
+
let sql = `CREATE DOMAIN public."${d.name}" AS ${d.base_type}`;
|
|
163
|
+
if (d.default_val)
|
|
164
|
+
sql += `\n DEFAULT ${d.default_val}`;
|
|
165
|
+
if (d.not_null)
|
|
166
|
+
sql += `\n NOT NULL`;
|
|
167
|
+
if (d.check_expr)
|
|
168
|
+
sql += `\n ${d.check_expr}`;
|
|
169
|
+
sql += ';';
|
|
170
|
+
result[d.name] = sql;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
async function extractExtensions(projectRef) {
|
|
175
|
+
const exts = await runQuery(projectRef, Queries.LIST_EXTENSIONS);
|
|
176
|
+
if (!Array.isArray(exts))
|
|
177
|
+
return {};
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const e of exts) {
|
|
180
|
+
// Always use IF NOT EXISTS so applying this snapshot is safe on any project
|
|
181
|
+
result[e.name] = `CREATE EXTENSION IF NOT EXISTS "${e.name}" WITH SCHEMA ${e.schema};`;
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
async function extractGrants(projectRef) {
|
|
186
|
+
const [tableGrants, functionGrants] = await Promise.all([
|
|
187
|
+
runQuery(projectRef, Queries.LIST_TABLE_GRANTS),
|
|
188
|
+
runQuery(projectRef, Queries.LIST_FUNCTION_GRANTS),
|
|
189
|
+
]);
|
|
190
|
+
// Group table grants by table name
|
|
191
|
+
const tableMap = {};
|
|
192
|
+
for (const g of tableGrants) {
|
|
193
|
+
if (!tableMap[g.table_name])
|
|
194
|
+
tableMap[g.table_name] = [];
|
|
195
|
+
const grantOption = g.is_grantable === 'YES' ? ' WITH GRANT OPTION' : '';
|
|
196
|
+
tableMap[g.table_name].push(`GRANT ${g.privileges} ON TABLE public."${g.table_name}" TO ${g.grantee}${grantOption};`);
|
|
197
|
+
}
|
|
198
|
+
// Group function grants by function name
|
|
199
|
+
const funcMap = {};
|
|
200
|
+
for (const g of functionGrants) {
|
|
201
|
+
if (!funcMap[g.routine_name])
|
|
202
|
+
funcMap[g.routine_name] = [];
|
|
203
|
+
const grantOption = g.is_grantable === 'YES' ? ' WITH GRANT OPTION' : '';
|
|
204
|
+
funcMap[g.routine_name].push(`GRANT ${g.privileges} ON FUNCTION public."${g.routine_name}" TO ${g.grantee}${grantOption};`);
|
|
205
|
+
}
|
|
206
|
+
const result = {};
|
|
207
|
+
// One file per table: "grants/table__tablename"
|
|
208
|
+
for (const [tableName, stmts] of Object.entries(tableMap)) {
|
|
209
|
+
result[`table__${tableName}`] = stmts.join('\n');
|
|
210
|
+
}
|
|
211
|
+
// One file per function: "grants/function__funcname"
|
|
212
|
+
for (const [funcName, stmts] of Object.entries(funcMap)) {
|
|
213
|
+
result[`function__${funcName}`] = stmts.join('\n');
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
async function extractPublications(projectRef) {
|
|
218
|
+
const pubs = await runQuery(projectRef, Queries.LIST_PUBLICATIONS);
|
|
219
|
+
if (!Array.isArray(pubs))
|
|
220
|
+
return {};
|
|
221
|
+
const result = {};
|
|
222
|
+
for (const p of pubs) {
|
|
223
|
+
const ops = [];
|
|
224
|
+
if (p.pubinsert)
|
|
225
|
+
ops.push('insert');
|
|
226
|
+
if (p.pubupdate)
|
|
227
|
+
ops.push('update');
|
|
228
|
+
if (p.pubdelete)
|
|
229
|
+
ops.push('delete');
|
|
230
|
+
if (p.pubtruncate)
|
|
231
|
+
ops.push('truncate');
|
|
232
|
+
const withClause = ops.length > 0 ? ` WITH (publish = '${ops.join(', ')}')` : '';
|
|
233
|
+
let sql;
|
|
234
|
+
if (p.puballtables) {
|
|
235
|
+
sql = `CREATE PUBLICATION "${p.pubname}" FOR ALL TABLES${withClause};`;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Fetch the specific tables in this publication
|
|
239
|
+
const tables = await runQuery(projectRef, Queries.LIST_PUBLICATION_TABLES(p.pubname));
|
|
240
|
+
if (tables.length === 0) {
|
|
241
|
+
sql = `CREATE PUBLICATION "${p.pubname}"${withClause};`;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const tableList = tables.map(t => `public."${t.table_name}"`).join(', ');
|
|
245
|
+
sql = `CREATE PUBLICATION "${p.pubname}" FOR TABLE ${tableList}${withClause};`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
result[p.pubname] = sql;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
async function extractSequences(projectRef) {
|
|
253
|
+
const seqs = await runQuery(projectRef, Queries.LIST_SEQUENCES);
|
|
254
|
+
if (!Array.isArray(seqs))
|
|
255
|
+
return {};
|
|
256
|
+
const result = {};
|
|
257
|
+
for (const s of seqs) {
|
|
258
|
+
const cycle = s.cycle_option === 'YES' ? 'CYCLE' : 'NO CYCLE';
|
|
259
|
+
result[s.name] = [
|
|
260
|
+
`CREATE SEQUENCE IF NOT EXISTS public."${s.name}"`,
|
|
261
|
+
` AS ${s.data_type}`,
|
|
262
|
+
` START WITH ${s.start_value}`,
|
|
263
|
+
` INCREMENT BY ${s.increment}`,
|
|
264
|
+
` MINVALUE ${s.minimum_value}`,
|
|
265
|
+
` MAXVALUE ${s.maximum_value}`,
|
|
266
|
+
` ${cycle};`,
|
|
267
|
+
].join('\n');
|
|
268
|
+
}
|
|
112
269
|
return result;
|
|
113
270
|
}
|
package/dist/schema/queries.js
CHANGED
|
@@ -1,17 +1,49 @@
|
|
|
1
|
+
// All dynamic queries return { text, values } objects to prevent SQL injection.
|
|
2
|
+
// Static queries (no user input) remain plain strings.
|
|
1
3
|
export const LIST_TABLES = `
|
|
2
4
|
SELECT c.relname as table_name, c.relrowsecurity as rls_enabled
|
|
3
5
|
FROM pg_class c
|
|
4
6
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
5
|
-
WHERE n.nspname = 'public'
|
|
7
|
+
WHERE n.nspname = 'public'
|
|
6
8
|
AND c.relkind = 'r'
|
|
7
9
|
ORDER BY c.relname;
|
|
8
10
|
`;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/** Parameterized — safe against SQL injection */
|
|
12
|
+
export const GET_COLUMNS = (tableName) => ({
|
|
13
|
+
text: `
|
|
14
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
15
|
+
FROM information_schema.columns
|
|
16
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
17
|
+
ORDER BY ordinal_position
|
|
18
|
+
`,
|
|
19
|
+
values: [tableName]
|
|
20
|
+
});
|
|
21
|
+
/** All constraints (PK, FK, UNIQUE, CHECK) for a table — parameterized */
|
|
22
|
+
export const GET_TABLE_CONSTRAINTS = (tableName) => ({
|
|
23
|
+
text: `
|
|
24
|
+
SELECT c.conname as name, pg_get_constraintdef(c.oid) as definition, c.contype
|
|
25
|
+
FROM pg_constraint c
|
|
26
|
+
JOIN pg_class t ON t.oid = c.conrelid
|
|
27
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
28
|
+
WHERE n.nspname = 'public' AND t.relname = $1
|
|
29
|
+
ORDER BY c.contype, c.conname
|
|
30
|
+
`,
|
|
31
|
+
values: [tableName]
|
|
32
|
+
});
|
|
33
|
+
/** Non-primary indexes for a table — parameterized */
|
|
34
|
+
export const GET_TABLE_INDEXES = (tableName) => ({
|
|
35
|
+
text: `
|
|
36
|
+
SELECT pg_get_indexdef(i.indexrelid) as definition
|
|
37
|
+
FROM pg_index i
|
|
38
|
+
JOIN pg_class t ON t.oid = i.indrelid
|
|
39
|
+
JOIN pg_class ix ON ix.oid = i.indexrelid
|
|
40
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
41
|
+
WHERE n.nspname = 'public' AND t.relname = $1
|
|
42
|
+
AND NOT i.indisprimary
|
|
43
|
+
ORDER BY ix.relname
|
|
44
|
+
`,
|
|
45
|
+
values: [tableName]
|
|
46
|
+
});
|
|
15
47
|
export const LIST_FUNCTIONS = `
|
|
16
48
|
SELECT proname as name, pg_get_functiondef(oid) as definition
|
|
17
49
|
FROM pg_proc
|
|
@@ -24,6 +56,12 @@ FROM information_schema.views
|
|
|
24
56
|
WHERE table_schema = 'public'
|
|
25
57
|
ORDER BY table_name;
|
|
26
58
|
`;
|
|
59
|
+
export const LIST_MAT_VIEWS = `
|
|
60
|
+
SELECT matviewname as name, pg_get_viewdef(matviewname::regclass) as definition
|
|
61
|
+
FROM pg_matviews
|
|
62
|
+
WHERE schemaname = 'public'
|
|
63
|
+
ORDER BY matviewname;
|
|
64
|
+
`;
|
|
27
65
|
export const LIST_TRIGGERS = `
|
|
28
66
|
SELECT tgname as name, pg_get_triggerdef(oid) as definition
|
|
29
67
|
FROM pg_trigger
|
|
@@ -37,7 +75,8 @@ FROM pg_policies
|
|
|
37
75
|
WHERE schemaname = 'public'
|
|
38
76
|
ORDER BY tablename, policyname;
|
|
39
77
|
`;
|
|
40
|
-
|
|
78
|
+
/** Enum types only */
|
|
79
|
+
export const LIST_ENUM_TYPES = `
|
|
41
80
|
SELECT t.typname as name, e.enumlabel as label
|
|
42
81
|
FROM pg_type t
|
|
43
82
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
@@ -45,3 +84,116 @@ JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
|
45
84
|
WHERE n.nspname = 'public'
|
|
46
85
|
ORDER BY t.typname, e.enumsortorder;
|
|
47
86
|
`;
|
|
87
|
+
/** Composite types */
|
|
88
|
+
export const LIST_COMPOSITE_TYPES = `
|
|
89
|
+
SELECT t.typname as name, a.attname as col_name,
|
|
90
|
+
pg_catalog.format_type(a.atttypid, a.atttypmod) as col_type
|
|
91
|
+
FROM pg_type t
|
|
92
|
+
JOIN pg_class c ON c.oid = t.typrelid
|
|
93
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
|
|
94
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
95
|
+
WHERE n.nspname = 'public' AND t.typtype = 'c'
|
|
96
|
+
ORDER BY t.typname, a.attnum;
|
|
97
|
+
`;
|
|
98
|
+
/** All sequences in the public schema */
|
|
99
|
+
export const LIST_SEQUENCES = `
|
|
100
|
+
SELECT sequence_name as name, data_type, start_value, minimum_value,
|
|
101
|
+
maximum_value, increment, cycle_option
|
|
102
|
+
FROM information_schema.sequences
|
|
103
|
+
WHERE sequence_schema = 'public'
|
|
104
|
+
ORDER BY sequence_name;
|
|
105
|
+
`;
|
|
106
|
+
/**
|
|
107
|
+
* All installed extensions (excluding the always-present plpgsql).
|
|
108
|
+
* Applied with CREATE EXTENSION IF NOT EXISTS — never dropped during revert.
|
|
109
|
+
*/
|
|
110
|
+
export const LIST_EXTENSIONS = `
|
|
111
|
+
SELECT e.extname as name, e.extversion as version,
|
|
112
|
+
n.nspname as schema
|
|
113
|
+
FROM pg_extension e
|
|
114
|
+
JOIN pg_namespace n ON n.oid = e.extnamespace
|
|
115
|
+
WHERE e.extname != 'plpgsql'
|
|
116
|
+
ORDER BY e.extname;
|
|
117
|
+
`;
|
|
118
|
+
/**
|
|
119
|
+
* All explicit grants on tables in the public schema.
|
|
120
|
+
* Excludes internal Supabase/Postgres system roles.
|
|
121
|
+
* Grouped so each row = one grantee + one table + all their privileges combined.
|
|
122
|
+
*/
|
|
123
|
+
export const LIST_TABLE_GRANTS = `
|
|
124
|
+
SELECT
|
|
125
|
+
table_name,
|
|
126
|
+
grantee,
|
|
127
|
+
string_agg(privilege_type, ', ' ORDER BY privilege_type) as privileges,
|
|
128
|
+
is_grantable
|
|
129
|
+
FROM information_schema.role_table_grants
|
|
130
|
+
WHERE table_schema = 'public'
|
|
131
|
+
AND grantee NOT IN (
|
|
132
|
+
'postgres', 'supabase_admin', 'supabase_auth_admin',
|
|
133
|
+
'supabase_storage_admin', 'dashboard_user', 'pg_monitor',
|
|
134
|
+
'pg_read_all_data', 'pg_write_all_data'
|
|
135
|
+
)
|
|
136
|
+
GROUP BY table_name, grantee, is_grantable
|
|
137
|
+
ORDER BY table_name, grantee;
|
|
138
|
+
`;
|
|
139
|
+
/**
|
|
140
|
+
* All explicit grants on functions in the public schema.
|
|
141
|
+
*/
|
|
142
|
+
export const LIST_FUNCTION_GRANTS = `
|
|
143
|
+
SELECT
|
|
144
|
+
routine_name,
|
|
145
|
+
grantee,
|
|
146
|
+
string_agg(privilege_type, ', ' ORDER BY privilege_type) as privileges,
|
|
147
|
+
is_grantable
|
|
148
|
+
FROM information_schema.role_routine_grants
|
|
149
|
+
WHERE routine_schema = 'public'
|
|
150
|
+
AND grantee NOT IN (
|
|
151
|
+
'postgres', 'supabase_admin', 'supabase_auth_admin',
|
|
152
|
+
'supabase_storage_admin', 'dashboard_user', 'pg_monitor',
|
|
153
|
+
'pg_read_all_data', 'pg_write_all_data'
|
|
154
|
+
)
|
|
155
|
+
GROUP BY routine_name, grantee, is_grantable
|
|
156
|
+
ORDER BY routine_name, grantee;
|
|
157
|
+
`;
|
|
158
|
+
/**
|
|
159
|
+
* All publications (used by Supabase Realtime).
|
|
160
|
+
*/
|
|
161
|
+
export const LIST_PUBLICATIONS = `
|
|
162
|
+
SELECT
|
|
163
|
+
p.pubname as name,
|
|
164
|
+
p.puballtables,
|
|
165
|
+
p.pubinsert,
|
|
166
|
+
p.pubupdate,
|
|
167
|
+
p.pubdelete,
|
|
168
|
+
p.pubtruncate
|
|
169
|
+
FROM pg_publication p
|
|
170
|
+
ORDER BY p.pubname;
|
|
171
|
+
`;
|
|
172
|
+
/** Tables enrolled in a specific publication — parameterized */
|
|
173
|
+
export const LIST_PUBLICATION_TABLES = (pubname) => ({
|
|
174
|
+
text: `
|
|
175
|
+
SELECT c.relname as table_name
|
|
176
|
+
FROM pg_publication p
|
|
177
|
+
JOIN pg_publication_rel pr ON pr.prpubid = p.oid
|
|
178
|
+
JOIN pg_class c ON c.oid = pr.prrelid
|
|
179
|
+
WHERE p.pubname = $1
|
|
180
|
+
ORDER BY c.relname
|
|
181
|
+
`,
|
|
182
|
+
values: [pubname]
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Domain types in the public schema (CREATE DOMAIN ... AS base_type CHECK ...).
|
|
186
|
+
*/
|
|
187
|
+
export const LIST_DOMAIN_TYPES = `
|
|
188
|
+
SELECT
|
|
189
|
+
t.typname as name,
|
|
190
|
+
pg_catalog.format_type(t.typbasetype, t.typtypmod) as base_type,
|
|
191
|
+
t.typnotnull as not_null,
|
|
192
|
+
t.typdefault as default_val,
|
|
193
|
+
pg_get_constraintdef(c.oid) as check_expr
|
|
194
|
+
FROM pg_type t
|
|
195
|
+
LEFT JOIN pg_constraint c ON c.contypid = t.oid AND c.contype = 'c'
|
|
196
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
197
|
+
WHERE n.nspname = 'public' AND t.typtype = 'd'
|
|
198
|
+
ORDER BY t.typname;
|
|
199
|
+
`;
|
package/dist/utils/hashing.js
CHANGED
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
import { parseSync, deparseSync } from 'pgsql-parser';
|
|
2
3
|
export function hashString(content) {
|
|
3
4
|
return crypto.createHash('sha1').update(content).digest('hex');
|
|
4
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Canonicalizes a SQL string using the actual PostgreSQL parser (libpg_query via WASM).
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* 1. Strip SQL comments (they are semantically empty).
|
|
11
|
+
* 2. Parse the SQL into an AST via pgsql-parser (the real PG C parser).
|
|
12
|
+
* 3. Deparse the AST back to SQL — this produces a structurally normalized form.
|
|
13
|
+
* 4. Apply aggressive whitespace normalization post-deparse:
|
|
14
|
+
* - Lowercase everything
|
|
15
|
+
* - Collapse all whitespace runs to a single space
|
|
16
|
+
* - Remove spaces adjacent to ( ) , ; = operators
|
|
17
|
+
* This eliminates the residual whitespace variants that pgsql-parser's
|
|
18
|
+
* deparser still allows through (e.g. "foo ( id" vs "foo(id").
|
|
19
|
+
*
|
|
20
|
+
* If the SQL is not a full parseable statement (e.g. a raw expression fragment
|
|
21
|
+
* stored in an old commit), the function falls back to best-effort text
|
|
22
|
+
* normalization and never throws.
|
|
23
|
+
*/
|
|
5
24
|
export function canonicalize(sql) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
25
|
+
if (!sql || sql.trim() === '')
|
|
26
|
+
return '';
|
|
27
|
+
// Strip comments first — they are semantically empty
|
|
28
|
+
const stripped = sql
|
|
29
|
+
.replace(/--[^\n]*/g, '') // single-line comments
|
|
30
|
+
.replace(/\/\*[\s\S]*?\*\//g, ''); // block comments
|
|
31
|
+
try {
|
|
32
|
+
const stmts = parseSync(stripped);
|
|
33
|
+
const deparsed = deparseSync(stmts);
|
|
34
|
+
return normalizeWhitespace(deparsed);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Fallback for fragments / non-parseable SQL (e.g. raw expressions)
|
|
38
|
+
return normalizeWhitespace(stripped);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Applies deterministic whitespace normalization to a SQL string.
|
|
43
|
+
* After this step, two strings that are semantically identical will be equal
|
|
44
|
+
* regardless of original indentation, casing, or punctuation spacing.
|
|
45
|
+
*/
|
|
46
|
+
function normalizeWhitespace(sql) {
|
|
47
|
+
return sql
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/\s+/g, ' ') // collapse all whitespace
|
|
50
|
+
.replace(/\s*\(\s*/g, '(') // no spaces around open-paren
|
|
51
|
+
.replace(/\s*\)\s*/g, ')') // no spaces around close-paren
|
|
52
|
+
.replace(/\s*,\s*/g, ',') // no spaces around comma
|
|
53
|
+
.replace(/\s*;\s*/g, ';') // no spaces around semicolon
|
|
54
|
+
.replace(/\s*=\s*/g, '=') // no spaces around equals
|
|
55
|
+
.replace(/\(\s+/g, '(') // extra pass: leading space in parens
|
|
56
|
+
.replace(/\s+\)/g, ')') // extra pass: trailing space in parens
|
|
57
|
+
.trim();
|
|
9
58
|
}
|