Skip to content

Commit 331ffc6

Browse files
fuergaosi233soedirgo
authored andcommitted
feat: add role config support
1 parent b787e81 commit 331ffc6

File tree

3 files changed

+135
-10
lines changed

3 files changed

+135
-10
lines changed

src/lib/PostgresMetaRoles.ts

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,22 @@ import { ident, literal } from 'pg-format'
22
import { DEFAULT_ROLES } from './constants'
33
import { rolesSql } from './sql'
44
import { PostgresMetaResult, PostgresRole } from './types'
5-
5+
export interface PostgresMetaRoleConfig {
6+
// https://www.rfc-editor.org/rfc/rfc6902
7+
op: 'remove' | 'add' | 'replace'
8+
path: string
9+
value?: string
10+
}
11+
export function changeRoleConfig2Object(config: string[]) {
12+
if (!config) {
13+
return null
14+
}
15+
return config.reduce((acc: any, cur) => {
16+
const [key, value] = cur.split('=')
17+
acc[key] = value
18+
return acc
19+
}, {})
20+
}
621
export default class PostgresMetaRoles {
722
query: (sql: string) => Promise<PostgresMetaResult<any>>
823

@@ -37,7 +52,14 @@ WHERE
3752
if (offset) {
3853
sql += ` OFFSET ${offset}`
3954
}
40-
return await this.query(sql)
55+
const result = await this.query(sql)
56+
if (result.data) {
57+
result.data = result.data.map((role: any) => {
58+
role.config = changeRoleConfig2Object(role.config)
59+
return role
60+
})
61+
}
62+
return result
4163
}
4264

4365
async retrieve({ id }: { id: number }): Promise<PostgresMetaResult<PostgresRole>>
@@ -52,11 +74,13 @@ WHERE
5274
if (id) {
5375
const sql = `${rolesSql} WHERE oid = ${literal(id)};`
5476
const { data, error } = await this.query(sql)
77+
5578
if (error) {
5679
return { data, error }
5780
} else if (data.length === 0) {
5881
return { data: null, error: { message: `Cannot find a role with ID ${id}` } }
5982
} else {
83+
data[0].config = changeRoleConfig2Object(data[0].config)
6084
return { data: data[0], error }
6185
}
6286
} else if (name) {
@@ -67,6 +91,7 @@ WHERE
6791
} else if (data.length === 0) {
6892
return { data: null, error: { message: `Cannot find a role named ${name}` } }
6993
} else {
94+
data[0].config = changeRoleConfig2Object(data[0].config)
7095
return { data: data[0], error }
7196
}
7297
} else {
@@ -89,6 +114,7 @@ WHERE
89114
member_of,
90115
members,
91116
admins,
117+
config,
92118
}: {
93119
name: string
94120
is_superuser?: boolean
@@ -104,6 +130,7 @@ WHERE
104130
member_of?: string[]
105131
members?: string[]
106132
admins?: string[]
133+
config?: Record<string, string>
107134
}): Promise<PostgresMetaResult<PostgresRole>> {
108135
const isSuperuserClause = is_superuser ? 'SUPERUSER' : 'NOSUPERUSER'
109136
const canCreateDbClause = can_create_db ? 'CREATEDB' : 'NOCREATEDB'
@@ -118,8 +145,20 @@ WHERE
118145
const memberOfClause = member_of === undefined ? '' : `IN ROLE ${member_of.join(',')}`
119146
const membersClause = members === undefined ? '' : `ROLE ${members.join(',')}`
120147
const adminsClause = admins === undefined ? '' : `ADMIN ${admins.join(',')}`
121-
148+
let configClause = ''
149+
if (config !== undefined) {
150+
configClause = Object.keys(config)
151+
.map((k) => {
152+
const v = config[k]
153+
if (!k || !v) {
154+
return ''
155+
}
156+
return `ALTER ROLE ${name} SET ${k} = ${v};`
157+
})
158+
.join('\n')
159+
}
122160
const sql = `
161+
BEGIN;
123162
CREATE ROLE ${ident(name)}
124163
WITH
125164
${isSuperuserClause}
@@ -134,7 +173,9 @@ WITH
134173
${validUntilClause}
135174
${memberOfClause}
136175
${membersClause}
137-
${adminsClause};`
176+
${adminsClause};
177+
${configClause ? configClause : ''}
178+
COMMIT;`
138179
const { error } = await this.query(sql)
139180
if (error) {
140181
return { data: null, error }
@@ -156,6 +197,7 @@ WITH
156197
connection_limit,
157198
password,
158199
valid_until,
200+
config,
159201
}: {
160202
name?: string
161203
is_superuser?: boolean
@@ -168,6 +210,7 @@ WITH
168210
connection_limit?: number
169211
password?: string
170212
valid_until?: string
213+
config?: PostgresMetaRoleConfig[]
171214
}
172215
): Promise<PostgresMetaResult<PostgresRole>> {
173216
const { data: old, error } = await this.retrieve({ id })
@@ -209,7 +252,27 @@ WITH
209252
connection_limit === undefined ? '' : `CONNECTION LIMIT ${connection_limit}`
210253
const passwordClause = password === undefined ? '' : `PASSWORD ${literal(password)}`
211254
const validUntilClause = valid_until === undefined ? '' : `VALID UNTIL ${literal(valid_until)}`
212-
255+
let configClause = ''
256+
if (config !== undefined) {
257+
const configSql = config.map((c) => {
258+
const { op, path, value } = c
259+
const k = path
260+
const v = value || null
261+
if (!k) {
262+
throw new Error(`Invalid config value ${value}`)
263+
}
264+
switch (op) {
265+
case 'add':
266+
case 'replace':
267+
return `ALTER ROLE ${ident(old!.name)} SET ${ident(k)} = ${literal(v)};`
268+
case 'remove':
269+
return `ALTER ROLE ${ident(old!.name)} RESET ${ident(k)};`
270+
default:
271+
throw new Error(`Invalid config op ${op}`)
272+
}
273+
})
274+
configClause = configSql.filter(Boolean).join('')
275+
}
213276
// nameSql must be last
214277
const sql = `
215278
BEGIN;
@@ -224,6 +287,7 @@ BEGIN;
224287
${connectionLimitClause}
225288
${passwordClause}
226289
${validUntilClause};
290+
${configClause ? configClause : ''}
227291
${nameSql}
228292
COMMIT;`
229293
{

src/lib/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,11 @@ export const postgresRoleSchema = Type.Object({
198198
connection_limit: Type.Integer(),
199199
password: Type.String(),
200200
valid_until: Type.Union([Type.String(), Type.Null()]),
201-
config: Type.Union([Type.String(), Type.Null()]),
201+
config: Type.Union([
202+
Type.String(),
203+
Type.Null(),
204+
Type.Record(Type.String(), Type.Union([Type.String()])),
205+
]),
202206
})
203207
export type PostgresRole = Static<typeof postgresRoleSchema>
204208

test/lib/roles.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ test('retrieve, create, update, delete', async () => {
4040
can_bypass_rls: true,
4141
connection_limit: 100,
4242
valid_until: '2020-01-01T00:00:00.000Z',
43+
config: { search_path: 'extension, public' },
4344
})
4445
expect(res).toMatchInlineSnapshot(
4546
{ data: { id: expect.any(Number) } },
@@ -51,7 +52,9 @@ test('retrieve, create, update, delete', async () => {
5152
"can_create_db": true,
5253
"can_create_role": true,
5354
"can_login": true,
54-
"config": null,
55+
"config": Object {
56+
"search_path": "extension, public",
57+
},
5558
"connection_limit": 100,
5659
"id": Any<Number>,
5760
"inherit_role": false,
@@ -76,7 +79,9 @@ test('retrieve, create, update, delete', async () => {
7679
"can_create_db": true,
7780
"can_create_role": true,
7881
"can_login": true,
79-
"config": null,
82+
"config": Object {
83+
"search_path": "extension, public",
84+
},
8085
"connection_limit": 100,
8186
"id": Any<Number>,
8287
"inherit_role": false,
@@ -105,6 +110,54 @@ test('retrieve, create, update, delete', async () => {
105110
can_bypass_rls: true,
106111
connection_limit: 100,
107112
valid_until: '2020-01-01T00:00:00.000Z',
113+
config: [
114+
{
115+
op: 'replace',
116+
path: 'search_path',
117+
value: 'public',
118+
},
119+
{
120+
op: 'add',
121+
path: 'log_statement',
122+
value: 'all',
123+
},
124+
],
125+
})
126+
expect(res).toMatchInlineSnapshot(
127+
{ data: { id: expect.any(Number) } },
128+
`
129+
Object {
130+
"data": Object {
131+
"active_connections": 0,
132+
"can_bypass_rls": true,
133+
"can_create_db": true,
134+
"can_create_role": true,
135+
"can_login": true,
136+
"config": Object {
137+
"log_statement": "all",
138+
"search_path": "public",
139+
},
140+
"connection_limit": 100,
141+
"id": Any<Number>,
142+
"inherit_role": false,
143+
"is_replication_role": true,
144+
"is_superuser": true,
145+
"name": "rr",
146+
"password": "********",
147+
"valid_until": "2020-01-01 00:00:00+00",
148+
},
149+
"error": null,
150+
}
151+
`
152+
)
153+
// Test remove config
154+
res = await pgMeta.roles.update(res.data!.id, {
155+
config: [
156+
{
157+
op: 'remove',
158+
path: 'log_statement',
159+
},
160+
],
108161
})
109162
expect(res).toMatchInlineSnapshot(
110163
{ data: { id: expect.any(Number) } },
@@ -116,7 +169,9 @@ test('retrieve, create, update, delete', async () => {
116169
"can_create_db": true,
117170
"can_create_role": true,
118171
"can_login": true,
119-
"config": null,
172+
"config": Object {
173+
"search_path": "public",
174+
},
120175
"connection_limit": 100,
121176
"id": Any<Number>,
122177
"inherit_role": false,
@@ -141,7 +196,9 @@ test('retrieve, create, update, delete', async () => {
141196
"can_create_db": true,
142197
"can_create_role": true,
143198
"can_login": true,
144-
"config": null,
199+
"config": Object {
200+
"search_path": "public",
201+
},
145202
"connection_limit": 100,
146203
"id": Any<Number>,
147204
"inherit_role": false,

0 commit comments

Comments
 (0)