Skip to content

Commit 2b2ce14

Browse files
committed
feat: Adds the ability to enable/disable RLS
closes #32
1 parent 9c1501d commit 2b2ce14

File tree

3 files changed

+80
-32
lines changed

3 files changed

+80
-32
lines changed

src/api/tables.ts

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Router } from 'express'
22
import SQL from 'sql-template-strings'
33
import sqlTemplates = require('../lib/sql')
44
const { columns, grants, policies, primary_keys, relationships, tables } = sqlTemplates
5-
import { coalesceRowsToArray } from '../lib/helpers'
5+
import { coalesceRowsToArray, toTransaction } from '../lib/helpers'
66
import { RunQuery } from '../lib/connectionPool'
77
import { DEFAULT_SYSTEM_SCHEMAS } from '../lib/constants'
88
import { Tables } from '../lib/interfaces'
@@ -18,7 +18,7 @@ const router = Router()
1818

1919
router.get('/', async (req, res) => {
2020
try {
21-
const sql = getTablesSqlize({ tables, columns, grants, policies, primary_keys, relationships })
21+
const sql = getTablesSql({ tables, columns, grants, policies, primary_keys, relationships })
2222
const { data } = await RunQuery(req.headers.pg, sql)
2323
const query: QueryParams = req.query
2424
const includeSystemSchemas = query?.includeSystemSchemas === 'true'
@@ -40,7 +40,9 @@ router.post('/', async (req, res) => {
4040

4141
// Create the table
4242
const createTableSql = createTable(name, schema)
43-
await RunQuery(req.headers.pg, createTableSql)
43+
const alterSql = alterTableSql(req.body)
44+
const transaction = toTransaction([createTableSql, alterSql])
45+
await RunQuery(req.headers.pg, transaction)
4446

4547
// Return fresh details
4648
const getTable = selectSingleByName(schema, name)
@@ -57,23 +59,26 @@ router.post('/', async (req, res) => {
5759
router.patch('/:id', async (req, res) => {
5860
try {
5961
const id: number = parseInt(req.params.id)
62+
if (!(id > 0)) throw new Error('id is required')
63+
6064
const name: string = req.body.name
65+
const payload: any = { ...req.body }
6166

6267
// Get table
6368
const getTableSql = selectSingleSql(id)
6469
const { data: getTableResults } = await RunQuery(req.headers.pg, getTableSql)
6570
let previousTable: Tables.Table = getTableResults[0]
6671

67-
// Update fields
68-
// NB: Run name updates last
69-
if (name) {
70-
const updateName = alterTableName(previousTable.name, name, previousTable.schema)
71-
await RunQuery(req.headers.pg, updateName)
72-
}
72+
// Update fields and name
73+
const nameSql = !name ? '' : alterTableName(previousTable.name, name, previousTable.schema)
74+
if (!name) payload.name = previousTable.name
75+
const alterSql = alterTableSql(payload)
76+
const transaction = toTransaction([nameSql, alterSql])
77+
await RunQuery(req.headers.pg, transaction)
7378

7479
// Return fresh details
75-
const { data: updatedResults } = await RunQuery(req.headers.pg, getTableSql)
76-
let updated: Tables.Table = updatedResults[0]
80+
const { data: freshTableData } = await RunQuery(req.headers.pg, getTableSql)
81+
let updated: Tables.Table = freshTableData[0]
7782
return res.status(200).json(updated)
7883
} catch (error) {
7984
// For this one, we always want to give back the error to the customer
@@ -90,7 +95,7 @@ router.delete('/:id', async (req, res) => {
9095
const { name, schema } = table
9196

9297
const cascade = req.query.cascade === 'true'
93-
const query = dropTableSqlize(schema, name, cascade)
98+
const query = dropTableSql(schema, name, cascade)
9499
await RunQuery(req.headers.pg, query)
95100

96101
return res.status(200).json(table)
@@ -100,7 +105,7 @@ router.delete('/:id', async (req, res) => {
100105
}
101106
})
102107

103-
const getTablesSqlize = ({
108+
const getTablesSql = ({
104109
tables,
105110
columns,
106111
grants,
@@ -142,24 +147,51 @@ SELECT
142147
OR (relationships.target_table_schema = tables.schema AND relationships.target_table_name = tables.name)`
143148
)}
144149
FROM
145-
tables`
150+
tables;`.trim()
146151
}
147152
const selectSingleSql = (id: number) => {
148-
return SQL``.append(tables).append(SQL` and c.oid = ${id}`)
153+
return `${tables} and c.oid = ${id};`.trim()
149154
}
150155
const selectSingleByName = (schema: string, name: string) => {
151-
return SQL``.append(tables).append(SQL` and table_schema = ${schema} and table_name = ${name}`)
156+
return `${tables} and table_schema = '${schema}' and table_name = '${name}';`.trim()
152157
}
153158
const createTable = (name: string, schema: string = 'postgres') => {
154-
const query = SQL``.append(`CREATE TABLE "${schema}"."${name}" ()`)
155-
return query
159+
return `CREATE TABLE IF NOT EXISTS "${schema}"."${name}" ();`.trim()
156160
}
157161
const alterTableName = (previousName: string, newName: string, schema: string) => {
158-
const query = SQL``.append(`ALTER TABLE "${schema}"."${previousName}" RENAME TO "${newName}"`)
159-
return query
162+
return `ALTER TABLE "${schema}"."${previousName}" RENAME TO "${newName}";`.trim()
163+
}
164+
const alterTableSql = ({
165+
schema = 'public',
166+
name,
167+
rls_enabled,
168+
rls_forced,
169+
}: {
170+
schema?: string
171+
name: string
172+
rls_enabled?: boolean
173+
rls_forced?: boolean
174+
}) => {
175+
let alter = `ALTER table "${schema}"."${name}"`
176+
let enableRls = ''
177+
if (rls_enabled !== undefined) {
178+
let enable = `${alter} ENABLE ROW LEVEL SECURITY;`
179+
let disable = `${alter} DISABLE ROW LEVEL SECURITY;`
180+
enableRls = rls_enabled ? enable : disable
181+
}
182+
let forceRls = ''
183+
if (rls_forced !== undefined) {
184+
let enable = `${alter} FORCE ROW LEVEL SECURITY;`
185+
let disable = `${alter} NO FORCE ROW LEVEL SECURITY;`
186+
forceRls = rls_forced ? enable : disable
187+
}
188+
return `
189+
${enableRls}
190+
${forceRls}
191+
`.trim()
160192
}
161-
const dropTableSqlize = (schema: string, name: string, cascade: boolean) => {
162-
return `DROP TABLE "${schema}"."${name}" ${cascade ? 'CASCADE' : 'RESTRICT'}`
193+
const dropTableSql = (schema: string, name: string, cascade: boolean) => {
194+
return `DROP TABLE "${schema}"."${name}" ${cascade ? 'CASCADE' : 'RESTRICT'};`.trim()
163195
}
164196
const removeSystemSchemas = (data: Tables.Table[]) => {
165197
return data.filter((x) => !DEFAULT_SYSTEM_SCHEMAS.includes(x.schema))

src/lib/helpers.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
export const coalesceRowsToArray = (source: string, joinQuery: string) => {
32
return `
43
COALESCE(
@@ -11,3 +10,16 @@ COALESCE(
1110
'[]'
1211
) AS ${source}`
1312
}
13+
14+
/**
15+
* Transforms an array of SQL strings into a transaction
16+
*/
17+
export const toTransaction = (statements: string[]) => {
18+
let cleansed = statements.map((x) => {
19+
let sql = x.trim()
20+
if (x.slice(-1) !== ';') sql += ';'
21+
return sql
22+
})
23+
let allStatements = cleansed.join('')
24+
return `BEGIN; ${allStatements} COMMIT;`
25+
}

test/integration/index.spec.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const STATUS = {
88
SUCCESS: 200,
99
ERROR: 500,
1010
}
11-
const PUBLIC_SCHEMA_ID = 2200
1211

1312
console.log('Running tests on ', URL)
1413

@@ -173,6 +172,8 @@ describe('/tables', async () => {
173172
assert.equal(tables.status, STATUS.SUCCESS)
174173
assert.equal(true, !!datum)
175174
assert.equal(true, !notIncluded)
175+
assert.equal(datum['rls_enabled'], false)
176+
assert.equal(datum['rls_forced'], false)
176177
})
177178
it('should return the columns, grants, and policies', async () => {
178179
const tables = await axios.get(`${URL}/tables`)
@@ -240,14 +241,17 @@ describe('/tables', async () => {
240241
await axios.delete(`${URL}/tables/${newTable.id}`)
241242
})
242243
it('PATCH /tables', async () => {
243-
const { data: newTable } = await axios.post(`${URL}/tables`, { name: 'test' })
244-
await axios.patch(`${URL}/tables/${newTable.id}`, { name: 'test a' })
245-
const { data: tables } = await axios.get(`${URL}/tables`)
246-
const updatedTableExists = tables.some(
247-
(table) => `${table.schema}.${table.name}` === `public.test a`
248-
)
249-
assert.equal(updatedTableExists, true)
250-
244+
const { data: newTable } = await axios.post(`${URL}/tables`, {
245+
name: 'test',
246+
})
247+
let { data: updatedTable } = await axios.patch(`${URL}/tables/${newTable.id}`, {
248+
name: 'test a',
249+
rls_enabled: true,
250+
rls_forced: true,
251+
})
252+
assert.equal(updatedTable.name, `test a`)
253+
assert.equal(updatedTable.rls_enabled, true)
254+
assert.equal(updatedTable.rls_forced, true)
251255
await axios.delete(`${URL}/tables/${newTable.id}`)
252256
})
253257
it('DELETE /tables', async () => {

0 commit comments

Comments
 (0)