@nifrajs/mcp-db 4.0.2 → 5.1.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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/pg-exec.d.ts +4 -7
- package/dist/internal/pg-exec.d.ts.map +1 -1
- package/dist/internal/pg-exec.js +13 -9
- package/dist/internal/pg-exec.js.map +1 -1
- package/dist/internal/shared.d.ts +22 -2
- package/dist/internal/shared.d.ts.map +1 -1
- package/dist/internal/shared.js +67 -4
- package/dist/internal/shared.js.map +1 -1
- package/dist/internal/sqlite-session.d.ts +11 -2
- package/dist/internal/sqlite-session.d.ts.map +1 -1
- package/dist/internal/sqlite-session.js +69 -15
- package/dist/internal/sqlite-session.js.map +1 -1
- package/dist/postgres.d.ts +4 -4
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +9 -14
- package/dist/postgres.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +47 -8
- package/src/internal/pg-exec.ts +17 -9
- package/src/internal/shared.ts +87 -7
- package/src/internal/sqlite-session.ts +89 -19
- package/src/postgres.ts +16 -20
package/src/internal/shared.ts
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
/** The structural slice of `bun:sqlite`'s `Database` this package needs. */
|
|
8
8
|
export interface SqliteDatabaseLike {
|
|
9
|
-
/** Prepare a statement; `all` runs it and returns row objects.
|
|
10
|
-
|
|
9
|
+
/** Prepare a statement; `all` runs it and returns row objects. `columnNames` and `iterate`, which
|
|
10
|
+
* bun:sqlite has, let `run_query` bound cell bytes and stop reading rows once the budget is spent. */
|
|
11
|
+
prepare(sql: string): {
|
|
12
|
+
all(...params: unknown[]): unknown[]
|
|
13
|
+
readonly columnNames?: readonly string[]
|
|
14
|
+
iterate?(): Iterable<unknown>
|
|
15
|
+
}
|
|
11
16
|
/** Execute a statement for its side effect (used only for `PRAGMA query_only`). */
|
|
12
17
|
run(sql: string): unknown
|
|
13
18
|
/** Optional path this database was opened from; a file-backed one can be reopened read-only in a
|
|
@@ -210,7 +215,7 @@ const isKeyword = (token: SqlToken | undefined, word: string): boolean =>
|
|
|
210
215
|
token?.bare === true && token.value.toLowerCase() === word
|
|
211
216
|
|
|
212
217
|
/**
|
|
213
|
-
* Every relation the statement names after `FROM`/`JOIN`, lowercased, with CTE names excluded.
|
|
218
|
+
* Every relation the statement names after `FROM`/`JOIN`/`IN`, lowercased, with CTE names excluded.
|
|
214
219
|
* A schema qualifier resolves to its table (`main.habits` -> `habits`); a subquery contributes
|
|
215
220
|
* nothing of its own because its inner `FROM` is tokenized alongside everything else.
|
|
216
221
|
*/
|
|
@@ -228,18 +233,21 @@ function relationNames(sql: string): string[] {
|
|
|
228
233
|
const names: string[] = []
|
|
229
234
|
const relationAt = (
|
|
230
235
|
index: number,
|
|
231
|
-
): { readonly name: string; readonly next: number } | undefined => {
|
|
236
|
+
): { readonly name: string; readonly qualified: boolean; readonly next: number } | undefined => {
|
|
232
237
|
const first = tokens[index]
|
|
233
238
|
if (first === undefined || first.kind !== "word") return undefined // `FROM (subquery)` - nothing to name here
|
|
234
239
|
// A qualified reference names the table second; an unqualified one names it first.
|
|
235
240
|
const qualified = tokens[index + 1]?.value === "." && tokens[index + 2]?.kind === "word"
|
|
236
241
|
const table = qualified ? (tokens[index + 2] as SqlToken).value : first.value
|
|
237
|
-
return { name: table.toLowerCase(), next: qualified ? index + 3 : index + 1 }
|
|
242
|
+
return { name: table.toLowerCase(), qualified, next: qualified ? index + 3 : index + 1 }
|
|
238
243
|
}
|
|
239
244
|
const addRelation = (index: number): number | undefined => {
|
|
240
245
|
const relation = relationAt(index)
|
|
241
246
|
if (relation === undefined) return undefined
|
|
242
|
-
|
|
247
|
+
// A CTE is never schema-qualified: `main.users` is the table even beside a CTE named `users`.
|
|
248
|
+
if (relation.name !== "" && (relation.qualified || !ctes.has(relation.name))) {
|
|
249
|
+
names.push(relation.name)
|
|
250
|
+
}
|
|
243
251
|
return relation.next
|
|
244
252
|
}
|
|
245
253
|
// Comma joins are part of the FROM table-source grammar but do not have a JOIN keyword. Walk the
|
|
@@ -260,6 +268,11 @@ function relationNames(sql: string): string[] {
|
|
|
260
268
|
"returning",
|
|
261
269
|
])
|
|
262
270
|
for (let i = 0; i < tokens.length; i++) {
|
|
271
|
+
// `x IN users` and `x IN json_each(...)` read a relation without a FROM.
|
|
272
|
+
if (isKeyword(tokens[i], "in")) {
|
|
273
|
+
addRelation(i + 1)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
263
276
|
const from = isKeyword(tokens[i], "from")
|
|
264
277
|
const join = isKeyword(tokens[i], "join")
|
|
265
278
|
if (!from && !join) continue
|
|
@@ -437,7 +450,7 @@ export type SqliteGate =
|
|
|
437
450
|
|
|
438
451
|
/**
|
|
439
452
|
* Gate one statement before SQLite sees it: non-empty, a single statement, SELECT/WITH only, and every
|
|
440
|
-
* relation it names after `FROM`/`JOIN` exposed. Returns the statement without its terminator, ready
|
|
453
|
+
* relation it names after `FROM`/`JOIN`/`IN` exposed. Returns the statement without its terminator, ready
|
|
441
454
|
* to wrap. Relation names are checked here as well as in the plan because SQLite's plan names an
|
|
442
455
|
* alias as the scan target (`users AS habits` plans as `SCAN habits`).
|
|
443
456
|
*/
|
|
@@ -504,12 +517,79 @@ export function unexposedPlanRelation(
|
|
|
504
517
|
return undefined
|
|
505
518
|
}
|
|
506
519
|
|
|
520
|
+
// The opcodes that open a b-tree cursor.
|
|
521
|
+
const BTREE_OPEN_OPCODES = new Set(["OpenRead", "OpenWrite", "ReopenIdx"])
|
|
522
|
+
|
|
523
|
+
const field = (row: unknown, key: string): unknown =>
|
|
524
|
+
typeof row === "object" && row !== null ? Reflect.get(row, key) : undefined
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* The first table a compiled statement opens that `exposed` rejects, or `undefined` when every b-tree
|
|
528
|
+
* it opens belongs to an exposed table (an index counts as its table). `program` is the statement's
|
|
529
|
+
* `EXPLAIN` rows and `catalog` is `SELECT tbl_name, rootpage FROM sqlite_master WHERE rootpage > 0`.
|
|
530
|
+
* The program names what SQLite will read by root page, so no alias, CTE name or quoting changes it.
|
|
531
|
+
* A virtual table or table-valued function (`VOpen`) names no table at all and is never exposed.
|
|
532
|
+
*/
|
|
533
|
+
export function unexposedProgramRelation(
|
|
534
|
+
program: readonly unknown[],
|
|
535
|
+
catalog: readonly unknown[],
|
|
536
|
+
exposed: (relation: string) => boolean,
|
|
537
|
+
): string | undefined {
|
|
538
|
+
const tables = new Map<number, string>([[1, "sqlite_master"]])
|
|
539
|
+
for (const entry of catalog) {
|
|
540
|
+
const table = field(entry, "tbl_name")
|
|
541
|
+
if (typeof table === "string") tables.set(Number(field(entry, "rootpage")), table)
|
|
542
|
+
}
|
|
543
|
+
for (const step of program) {
|
|
544
|
+
const opcode = field(step, "opcode")
|
|
545
|
+
if (opcode === "VOpen") return "<virtual table>"
|
|
546
|
+
if (typeof opcode !== "string" || !BTREE_OPEN_OPCODES.has(opcode)) continue
|
|
547
|
+
// P3 is the schema: 0 is main; temp (1) and attached databases (2+) are not in this catalog.
|
|
548
|
+
const schema = Number(field(step, "p3"))
|
|
549
|
+
if (schema !== 0) return `<schema ${schema}>`
|
|
550
|
+
const page = Number(field(step, "p2"))
|
|
551
|
+
const table = tables.get(page)
|
|
552
|
+
if (table === undefined) return `<root page ${page}>`
|
|
553
|
+
if (!exposed(table.toLowerCase())) return table
|
|
554
|
+
}
|
|
555
|
+
return undefined
|
|
556
|
+
}
|
|
557
|
+
|
|
507
558
|
/** Wrap a gated query so SQLite materializes at most one row beyond `maxRows`. */
|
|
508
559
|
export function boundedSqliteQuery(query: string, maxRows: number): string {
|
|
509
560
|
// nifra-expect sql-dynamic: wraps a query already through gateSqliteStatement in a bounding subselect; it runs on a query-only connection
|
|
510
561
|
return `SELECT * FROM (${query}) AS "__nifra_result" LIMIT ${maxRows + 1}`
|
|
511
562
|
}
|
|
512
563
|
|
|
564
|
+
/** The column {@link cellBoundedSqliteQuery} adds to carry each row's cell bytes. */
|
|
565
|
+
export const ROW_BYTES_COLUMN = "__nifra_row_bytes"
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* {@link boundedSqliteQuery}, plus two bounds the driver cannot apply itself: a text or blob cell
|
|
569
|
+
* longer than `maxCellBytes` arrives as `"<n bytes>"` instead of being copied out, and each row
|
|
570
|
+
* carries its cells' byte total in {@link ROW_BYTES_COLUMN} so a reader can stop once the result
|
|
571
|
+
* budget is spent. `columns` are the result column names of the bounded query.
|
|
572
|
+
*/
|
|
573
|
+
export function cellBoundedSqliteQuery(
|
|
574
|
+
query: string,
|
|
575
|
+
columns: readonly string[],
|
|
576
|
+
maxRows: number,
|
|
577
|
+
maxCellBytes: number,
|
|
578
|
+
): string {
|
|
579
|
+
const selected: string[] = []
|
|
580
|
+
const sizes: string[] = []
|
|
581
|
+
for (const name of columns) {
|
|
582
|
+
const column = `"${name.replaceAll('"', '""')}"`
|
|
583
|
+
const bytes = `length(CAST(${column} AS BLOB))`
|
|
584
|
+
selected.push(
|
|
585
|
+
`CASE WHEN typeof(${column}) IN ('text', 'blob') AND ${bytes} > ${maxCellBytes} THEN '<' || ${bytes} || ' bytes>' ELSE ${column} END AS ${column}`,
|
|
586
|
+
)
|
|
587
|
+
sizes.push(`min(coalesce(${bytes}, 0), ${maxCellBytes})`)
|
|
588
|
+
}
|
|
589
|
+
// nifra-expect sql-dynamic: wraps a query already through gateSqliteStatement; column names are quoted identifiers and the bounds are validated integers
|
|
590
|
+
return `SELECT ${selected.join(", ")}, ${sizes.join(" + ")} AS "${ROW_BYTES_COLUMN}" FROM (${query}) AS "__nifra_result" LIMIT ${maxRows + 1}`
|
|
591
|
+
}
|
|
592
|
+
|
|
513
593
|
/** Wrap a gated query to count its rows (re-executes it). */
|
|
514
594
|
export function countSqliteQuery(query: string): string {
|
|
515
595
|
// nifra-expect sql-dynamic: wraps a query already through gateSqliteStatement in a counting subselect; it runs on a query-only connection
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* tool runs each query in its own subprocess instead, which bounds the work by killing the process.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { ROW_BYTES_COLUMN, type SqliteDatabaseLike } from "./shared.ts"
|
|
8
8
|
|
|
9
9
|
export class QueryTimeoutError extends Error {
|
|
10
10
|
constructor() {
|
|
@@ -13,9 +13,19 @@ export class QueryTimeoutError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** Rows read until their bytes passed the budget; `cut` says reading stopped early. */
|
|
17
|
+
export interface BoundedRows {
|
|
18
|
+
readonly rows: unknown[]
|
|
19
|
+
readonly cut: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One execution lane for `run_query`: each call resolves, or rejects once the deadline passes. */
|
|
17
23
|
export interface QuerySession {
|
|
18
24
|
run(sql: string, deadline: number): Promise<unknown[]>
|
|
25
|
+
/** The result column names of `sql`, without running it. Empty when the driver cannot say. */
|
|
26
|
+
columns(sql: string, deadline: number): Promise<readonly string[]>
|
|
27
|
+
/** Rows of a `cellBoundedSqliteQuery`, read one at a time until their bytes pass `maxBytes`. */
|
|
28
|
+
read(sql: string, deadline: number, maxBytes: number): Promise<BoundedRows>
|
|
19
29
|
close(): Promise<void>
|
|
20
30
|
}
|
|
21
31
|
|
|
@@ -29,7 +39,7 @@ const SQLITE_WORKER_SOURCE = `
|
|
|
29
39
|
const { Database } = require("bun:sqlite")
|
|
30
40
|
let db
|
|
31
41
|
self.onmessage = (event) => {
|
|
32
|
-
const { id, filename, sql, type } = event.data
|
|
42
|
+
const { id, filename, sql, type, maxBytes } = event.data
|
|
33
43
|
if (type === "close") {
|
|
34
44
|
let response
|
|
35
45
|
try {
|
|
@@ -54,7 +64,30 @@ self.onmessage = (event) => {
|
|
|
54
64
|
db = new Database(filename, { readonly: true })
|
|
55
65
|
db.run("PRAGMA query_only = ON")
|
|
56
66
|
}
|
|
57
|
-
|
|
67
|
+
const statement = db.prepare(sql)
|
|
68
|
+
if (type === "columns") {
|
|
69
|
+
const rows = statement.columnNames
|
|
70
|
+
statement.finalize()
|
|
71
|
+
self.postMessage({ id, ok: true, rows })
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (type === "read") {
|
|
75
|
+
const rows = []
|
|
76
|
+
let bytes = 0
|
|
77
|
+
let cut = false
|
|
78
|
+
for (const { ${ROW_BYTES_COLUMN}: size, ...row } of statement.iterate()) {
|
|
79
|
+
rows.push(row)
|
|
80
|
+
bytes += Number(size)
|
|
81
|
+
if (bytes > maxBytes) {
|
|
82
|
+
cut = true
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
statement.finalize()
|
|
87
|
+
self.postMessage({ id, ok: true, rows, cut })
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
self.postMessage({ id, ok: true, rows: statement.all() })
|
|
58
91
|
} catch (error) {
|
|
59
92
|
self.postMessage({ id, ok: false, error: String(error) })
|
|
60
93
|
}
|
|
@@ -75,10 +108,16 @@ function reopenableFilename(db: SqliteDatabaseLike): string | undefined {
|
|
|
75
108
|
|
|
76
109
|
interface PendingQuery {
|
|
77
110
|
readonly deadline: number
|
|
78
|
-
resolve(
|
|
111
|
+
resolve(result: BoundedRows): void
|
|
79
112
|
reject(error: Error): void
|
|
80
113
|
}
|
|
81
114
|
|
|
115
|
+
interface WorkerQuery {
|
|
116
|
+
readonly sql: string
|
|
117
|
+
readonly type?: "columns" | "read"
|
|
118
|
+
readonly maxBytes?: number
|
|
119
|
+
}
|
|
120
|
+
|
|
82
121
|
interface PendingClose {
|
|
83
122
|
readonly id: number
|
|
84
123
|
resolve(graceful?: boolean): void
|
|
@@ -135,11 +174,12 @@ function workerSession(filename: string): QuerySession {
|
|
|
135
174
|
id?: number
|
|
136
175
|
ok?: boolean
|
|
137
176
|
rows?: unknown[]
|
|
177
|
+
cut?: boolean
|
|
138
178
|
error?: string
|
|
139
179
|
closed?: boolean
|
|
140
180
|
}>,
|
|
141
181
|
) => {
|
|
142
|
-
const { id, ok, rows, error } = event.data
|
|
182
|
+
const { id, ok, rows, cut, error } = event.data
|
|
143
183
|
if (typeof id !== "number") return
|
|
144
184
|
if (event.data.closed === true) {
|
|
145
185
|
const closing = pendingClose
|
|
@@ -155,7 +195,7 @@ function workerSession(filename: string): QuerySession {
|
|
|
155
195
|
return
|
|
156
196
|
}
|
|
157
197
|
pending.delete(id)
|
|
158
|
-
if (ok === true) waiter.resolve(rows ?? [])
|
|
198
|
+
if (ok === true) waiter.resolve({ rows: rows ?? [], cut: cut === true })
|
|
159
199
|
else waiter.reject(new Error(error ?? "query failed in worker"))
|
|
160
200
|
}
|
|
161
201
|
spawned.onerror = () => discard(new Error("query failed in worker"))
|
|
@@ -166,7 +206,7 @@ function workerSession(filename: string): QuerySession {
|
|
|
166
206
|
return spawned
|
|
167
207
|
}
|
|
168
208
|
|
|
169
|
-
const dispatch = (
|
|
209
|
+
const dispatch = (query: WorkerQuery, deadline: number): Promise<BoundedRows> => {
|
|
170
210
|
if (closed) return Promise.reject(new Error("query session is closed"))
|
|
171
211
|
const active = ensureWorker()
|
|
172
212
|
const remaining = deadline - Date.now()
|
|
@@ -175,13 +215,13 @@ function workerSession(filename: string): QuerySession {
|
|
|
175
215
|
return Promise.reject(new QueryTimeoutError())
|
|
176
216
|
}
|
|
177
217
|
const id = ++nextId
|
|
178
|
-
return new Promise<
|
|
218
|
+
return new Promise<BoundedRows>((resolve, reject) => {
|
|
179
219
|
const timer = setTimeout(() => discard(new QueryTimeoutError()), remaining)
|
|
180
220
|
pending.set(id, {
|
|
181
221
|
deadline,
|
|
182
|
-
resolve: (
|
|
222
|
+
resolve: (result) => {
|
|
183
223
|
clearTimeout(timer)
|
|
184
|
-
resolve(
|
|
224
|
+
resolve(result)
|
|
185
225
|
},
|
|
186
226
|
reject: (error) => {
|
|
187
227
|
clearTimeout(timer)
|
|
@@ -189,7 +229,7 @@ function workerSession(filename: string): QuerySession {
|
|
|
189
229
|
},
|
|
190
230
|
})
|
|
191
231
|
try {
|
|
192
|
-
active.postMessage({ id, filename,
|
|
232
|
+
active.postMessage({ id, filename, ...query })
|
|
193
233
|
} catch (error) {
|
|
194
234
|
pending.delete(id)
|
|
195
235
|
clearTimeout(timer)
|
|
@@ -198,14 +238,21 @@ function workerSession(filename: string): QuerySession {
|
|
|
198
238
|
})
|
|
199
239
|
}
|
|
200
240
|
|
|
241
|
+
// One worker owns the connection, so overlapping calls queue behind each other rather than
|
|
242
|
+
// interleaving statements on it.
|
|
243
|
+
const queue = (query: WorkerQuery, deadline: number): Promise<BoundedRows> => {
|
|
244
|
+
const result = lane.then(() => dispatch(query, deadline))
|
|
245
|
+
lane = result.catch(() => undefined)
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
248
|
+
|
|
201
249
|
return {
|
|
202
|
-
run(sql, deadline) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
},
|
|
250
|
+
run: async (sql, deadline) => (await queue({ sql }, deadline)).rows,
|
|
251
|
+
columns: async (sql, deadline) =>
|
|
252
|
+
(await queue({ sql, type: "columns" }, deadline)).rows.filter(
|
|
253
|
+
(name): name is string => typeof name === "string",
|
|
254
|
+
),
|
|
255
|
+
read: (sql, deadline, maxBytes) => queue({ sql, type: "read", maxBytes }, deadline),
|
|
209
256
|
close() {
|
|
210
257
|
if (closePromise !== undefined) return closePromise
|
|
211
258
|
closed = true
|
|
@@ -257,6 +304,29 @@ function inProcessSession(db: SqliteDatabaseLike): QuerySession {
|
|
|
257
304
|
if (Date.now() > deadline) throw new QueryTimeoutError()
|
|
258
305
|
return rows
|
|
259
306
|
},
|
|
307
|
+
async columns(sql, deadline) {
|
|
308
|
+
if (Date.now() >= deadline) throw new QueryTimeoutError()
|
|
309
|
+
return db.prepare(sql).columnNames ?? []
|
|
310
|
+
},
|
|
311
|
+
async read(sql, deadline, maxBytes) {
|
|
312
|
+
if (Date.now() >= deadline) throw new QueryTimeoutError()
|
|
313
|
+
const statement = db.prepare(sql)
|
|
314
|
+
const rows: unknown[] = []
|
|
315
|
+
let bytes = 0
|
|
316
|
+
let cut = false
|
|
317
|
+
for (const row of statement.iterate?.() ?? statement.all()) {
|
|
318
|
+
if (typeof row !== "object" || row === null) continue
|
|
319
|
+
bytes += Number(Reflect.get(row, ROW_BYTES_COLUMN))
|
|
320
|
+
Reflect.deleteProperty(row, ROW_BYTES_COLUMN)
|
|
321
|
+
rows.push(row)
|
|
322
|
+
if (bytes > maxBytes) {
|
|
323
|
+
cut = true
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (Date.now() > deadline) throw new QueryTimeoutError()
|
|
328
|
+
return { rows, cut }
|
|
329
|
+
},
|
|
260
330
|
async close() {},
|
|
261
331
|
}
|
|
262
332
|
}
|
package/src/postgres.ts
CHANGED
|
@@ -494,7 +494,7 @@ export async function explainPostgres(
|
|
|
494
494
|
const explained = await gated(client, sql, options, (connection, statement) =>
|
|
495
495
|
explainThroughServer(connection, statement, {
|
|
496
496
|
analyze: options.analyze === true,
|
|
497
|
-
scope: checkScope(options),
|
|
497
|
+
scope: checkScope(options, options.redaction?.column),
|
|
498
498
|
}),
|
|
499
499
|
)
|
|
500
500
|
if (isDbRefusal(explained)) return explained
|
|
@@ -709,10 +709,10 @@ export interface PostgresRoleSql {
|
|
|
709
709
|
}
|
|
710
710
|
|
|
711
711
|
/**
|
|
712
|
-
* Write the SQL for a login role that can read the allowed schemas and nothing else: `
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
712
|
+
* Write the SQL for a login role that can read the allowed schemas and nothing else: `GRANT SELECT`
|
|
713
|
+
* per schema plus default privileges, and a `REVOKE` for each excluded table and its partitions.
|
|
714
|
+
* `pg_read_all_data` is never used: it reaches every schema, present and future, and overrides a
|
|
715
|
+
* REVOKE. Nothing is executed.
|
|
716
716
|
*/
|
|
717
717
|
export async function postgresRoleSql(
|
|
718
718
|
client: PostgresClient,
|
|
@@ -756,21 +756,17 @@ export async function postgresRoleSql(
|
|
|
756
756
|
`ALTER ROLE ${who} SET default_transaction_read_only = on;`,
|
|
757
757
|
`GRANT CONNECT ON DATABASE ${quoteIdent(database)} TO ${who};`,
|
|
758
758
|
]
|
|
759
|
-
|
|
760
|
-
statements.push(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
statements.push(
|
|
771
|
-
`REVOKE SELECT ON TABLE ${quoteIdent(String(row.schema))}.${quoteIdent(String(row.name))} FROM ${who};`,
|
|
772
|
-
)
|
|
773
|
-
}
|
|
759
|
+
for (const schema of schemas) {
|
|
760
|
+
statements.push(
|
|
761
|
+
`GRANT USAGE ON SCHEMA ${quoteIdent(schema)} TO ${who};`,
|
|
762
|
+
`GRANT SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(schema)} TO ${who};`,
|
|
763
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schema)} GRANT SELECT ON TABLES TO ${who};`,
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
for (const row of excluded) {
|
|
767
|
+
statements.push(
|
|
768
|
+
`REVOKE SELECT ON TABLE ${quoteIdent(String(row.schema))}.${quoteIdent(String(row.name))} FROM ${who};`,
|
|
769
|
+
)
|
|
774
770
|
}
|
|
775
771
|
return { role, database, serverVersionNum: version, statements }
|
|
776
772
|
})
|