Skip to content

Commit 14a6a35

Browse files
authored
Merge pull request #67 from supabase/feat/publications
Add support for /publications
2 parents 6c1257b + 9f3f9b1 commit 14a6a35

File tree

8 files changed

+291
-5
lines changed

8 files changed

+291
-5
lines changed

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@types/express": "^4.17.6",
3535
"@types/node": "^14.0.13",
3636
"@types/pg": "^7.14.3",
37+
"@types/pg-format": "^1.0.1",
3738
"axios": "^0.19.2",
3839
"esm": "^3.2.25",
3940
"mocha": "^7.1.2",

src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ router.use('/columns', addConnectionToRequest, require('./api/columns'))
99
router.use('/extensions', addConnectionToRequest, require('./api/extensions'))
1010
router.use('/functions', addConnectionToRequest, require('./api/functions'))
1111
router.use('/policies', addConnectionToRequest, require('./api/policies'))
12+
router.use('/publications', addConnectionToRequest, require('./api/publications'))
1213
router.use('/query', addConnectionToRequest, require('./api/query'))
1314
router.use('/schemas', addConnectionToRequest, require('./api/schemas'))
1415
router.use('/tables', addConnectionToRequest, require('./api/tables'))

src/api/publications.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { Router } from 'express'
2+
import { ident, literal } from 'pg-format'
3+
import sql = require('../lib/sql')
4+
import { RunQuery } from '../lib/connectionPool'
5+
6+
const { publications } = sql
7+
const router = Router()
8+
9+
router.get('/', async (req, res) => {
10+
try {
11+
const { data } = await RunQuery(req.headers.pg, publications)
12+
return res.status(200).json(data)
13+
} catch (error) {
14+
res.status(500).json({ error: error.message })
15+
}
16+
})
17+
18+
router.post('/', async (req, res) => {
19+
try {
20+
const createPublicationSql = createPublicationSqlize(req.body)
21+
await RunQuery(req.headers.pg, createPublicationSql)
22+
23+
const getPublicationSql = getPublicationByNameSqlize(req.body.name)
24+
const newPublication = (await RunQuery(req.headers.pg, getPublicationSql)).data[0]
25+
return res.status(200).json(newPublication)
26+
} catch (error) {
27+
res.status(400).json({ error: error.message })
28+
}
29+
})
30+
31+
router.patch('/:id', async (req, res) => {
32+
try {
33+
const id = req.params.id
34+
const getPublicationSql = getPublicationByIdSqlize(id)
35+
const oldPublication = (await RunQuery(req.headers.pg, getPublicationSql)).data[0]
36+
37+
const args = req.body
38+
const alterPublicationSql = alterPublicationSqlize({ oldPublication, ...args })
39+
await RunQuery(req.headers.pg, alterPublicationSql)
40+
41+
const updatedPublication = (await RunQuery(req.headers.pg, getPublicationSql)).data[0]
42+
return res.status(200).json(updatedPublication)
43+
} catch (error) {
44+
console.log(error.message)
45+
if (error instanceof TypeError) {
46+
res.status(404).json({ error: 'Cannot find a publication with that id' })
47+
} else {
48+
res.status(400).json({ error: error.message })
49+
}
50+
}
51+
})
52+
53+
router.delete('/:id', async (req, res) => {
54+
try {
55+
const id = req.params.id
56+
const getPublicationSql = getPublicationByIdSqlize(id)
57+
const publication = (await RunQuery(req.headers.pg, getPublicationSql)).data[0]
58+
const { name } = publication
59+
60+
const dropPublicationSql = dropPublicationSqlize(name)
61+
await RunQuery(req.headers.pg, dropPublicationSql)
62+
63+
return res.status(200).json(publication)
64+
} catch (error) {
65+
if (error instanceof TypeError) {
66+
res.status(404).json({ error: 'Cannot find a publication with that id' })
67+
} else {
68+
res.status(400).json({ error: error.message })
69+
}
70+
}
71+
})
72+
73+
const createPublicationSqlize = ({
74+
name,
75+
publish_insert = false,
76+
publish_update = false,
77+
publish_delete = false,
78+
publish_truncate = false,
79+
tables,
80+
}: {
81+
name: string
82+
publish_insert?: boolean
83+
publish_update?: boolean
84+
publish_delete?: boolean
85+
publish_truncate?: boolean
86+
tables?: string[]
87+
}) => {
88+
let tableClause: string
89+
if (tables === undefined) {
90+
tableClause = 'FOR ALL TABLES'
91+
} else if (tables.length === 0) {
92+
tableClause = ''
93+
} else {
94+
tableClause = `FOR TABLE ${tables.map(ident).join(',')}`
95+
}
96+
97+
let publishOps = []
98+
if (publish_insert) publishOps.push('insert')
99+
if (publish_update) publishOps.push('update')
100+
if (publish_delete) publishOps.push('delete')
101+
if (publish_truncate) publishOps.push('truncate')
102+
103+
return `
104+
CREATE PUBLICATION ${ident(name)} ${tableClause}
105+
WITH (publish = '${publishOps.join(',')}')
106+
`
107+
}
108+
109+
const getPublicationByNameSqlize = (name: string) => {
110+
return `${publications} WHERE p.pubname = ${literal(name)}`
111+
}
112+
113+
const getPublicationByIdSqlize = (id: string) => {
114+
return `${publications} WHERE p.oid = ${literal(id)}`
115+
}
116+
117+
const alterPublicationSqlize = ({
118+
oldPublication,
119+
name,
120+
owner,
121+
publish_insert,
122+
publish_update,
123+
publish_delete,
124+
publish_truncate,
125+
tables,
126+
}: {
127+
oldPublication: any
128+
name?: string
129+
owner?: string
130+
publish_insert?: boolean
131+
publish_update?: boolean
132+
publish_delete?: boolean
133+
publish_truncate?: boolean
134+
tables?: string[]
135+
}) => {
136+
// Need to work around the limitations of the SQL. Can't add/drop tables from
137+
// a publication with FOR ALL TABLES. Can't use the SET TABLE clause without
138+
// at least one table.
139+
//
140+
// new tables
141+
//
142+
// | undefined | string[] |
143+
// ---------|-----------|-----------------|
144+
// null | '' | 400 Bad Request |
145+
// old tables ---------|-----------|-----------------|
146+
// string[] | '' | See below |
147+
//
148+
// new tables
149+
//
150+
// | [] | [...] |
151+
// ---------|-----------|-----------------|
152+
// [] | '' | SET TABLE |
153+
// old tables ---------|-----------|-----------------|
154+
// [...] | DROP all | SET TABLE |
155+
//
156+
let tableSql: string
157+
if (tables === undefined) {
158+
tableSql = ''
159+
} else if (oldPublication.tables === null) {
160+
throw Error('Tables cannot be added to or dropped from FOR ALL TABLES publications')
161+
} else if (tables.length > 0) {
162+
tableSql = `ALTER PUBLICATION ${ident(oldPublication.name)} SET TABLE ${tables
163+
.map(ident)
164+
.join(',')}`
165+
} else if (oldPublication.tables.length === 0) {
166+
tableSql = ''
167+
} else {
168+
tableSql = `ALTER PUBLICATION ${ident(
169+
oldPublication.name
170+
)} DROP TABLE ${oldPublication.tables.map(ident).join(',')}`
171+
}
172+
173+
let publishOps = []
174+
if (publish_insert ?? oldPublication.publish_insert) publishOps.push('insert')
175+
if (publish_update ?? oldPublication.publish_update) publishOps.push('update')
176+
if (publish_delete ?? oldPublication.publish_delete) publishOps.push('delete')
177+
if (publish_truncate ?? oldPublication.publish_truncate) publishOps.push('truncate')
178+
const publishSql = `ALTER PUBLICATION ${ident(
179+
oldPublication.name
180+
)} SET (publish = '${publishOps.join(',')}')`
181+
182+
const ownerSql =
183+
owner === undefined
184+
? ''
185+
: `ALTER PUBLICATION ${ident(oldPublication.name)} OWNER TO ${ident(owner)}`
186+
187+
const nameSql =
188+
name === undefined || name === oldPublication.name
189+
? ''
190+
: `ALTER PUBLICATION ${ident(oldPublication.name)} RENAME TO ${ident(name)}`
191+
192+
// nameSql must be last
193+
return `
194+
BEGIN;
195+
${tableSql};
196+
${publishSql};
197+
${ownerSql};
198+
${nameSql};
199+
COMMIT;
200+
`
201+
}
202+
203+
const dropPublicationSqlize = (name: string) => {
204+
return `DROP PUBLICATION IF EXISTS ${ident(name)}`
205+
}
206+
207+
export = router

src/lib/sql/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ export = {
99
functions: fs.readFileSync(path.join(__dirname, '/functions.sql')).toString(),
1010
grants: fs.readFileSync(path.join(__dirname, '/grants.sql')).toString(),
1111
joins: fs.readFileSync(path.join(__dirname, '/joins.sql')).toString(),
12-
primary_keys: fs.readFileSync(path.join(__dirname, '/primary_keys.sql')).toString(),
1312
policies: fs.readFileSync(path.join(__dirname, '/policies.sql')).toString(),
13+
primary_keys: fs.readFileSync(path.join(__dirname, '/primary_keys.sql')).toString(),
14+
publications: fs.readFileSync(path.join(__dirname, '/publications.sql')).toString(),
1415
relationships: fs.readFileSync(path.join(__dirname, '/relationships.sql')).toString(),
1516
roles: fs.readFileSync(path.join(__dirname, '/roles.sql')).toString(),
1617
schemas: fs.readFileSync(path.join(__dirname, '/schemas.sql')).toString(),

src/lib/sql/publications.sql

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
SELECT
2+
p.oid AS id,
3+
p.pubname AS name,
4+
p.pubowner :: regrole AS owner,
5+
p.pubinsert AS publish_insert,
6+
p.pubupdate AS publish_update,
7+
p.pubdelete AS publish_delete,
8+
p.pubtruncate AS publish_truncate,
9+
CASE
10+
WHEN p.puballtables THEN NULL
11+
ELSE pr.tables
12+
END AS tables
13+
FROM
14+
pg_catalog.pg_publication AS p
15+
LEFT JOIN LATERAL (
16+
SELECT
17+
COALESCE(
18+
array_agg(pr.prrelid :: regclass :: text) filter (
19+
WHERE
20+
pr.prrelid IS NOT NULL
21+
),
22+
'{}'
23+
) AS tables
24+
FROM
25+
pg_catalog.pg_publication_rel AS pr
26+
WHERE
27+
pr.prpubid = p.oid
28+
) AS pr ON 1 = 1

test/integration/index.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,49 @@ describe('/policies', () => {
612612
assert.equal(stillExists, false, 'Policy is deleted')
613613
})
614614
})
615+
describe('/publications', () => {
616+
const publication = {
617+
name: 'a',
618+
publish_insert: true,
619+
publish_update: true,
620+
publish_delete: true,
621+
publish_truncate: false,
622+
tables: ['users'],
623+
}
624+
it('POST', async () => {
625+
const { data: newPublication } = await axios.post(`${URL}/publications`, publication)
626+
assert.equal(newPublication.name, publication.name)
627+
assert.equal(newPublication.publish_insert, publication.publish_insert)
628+
assert.equal(newPublication.publish_update, publication.publish_update)
629+
assert.equal(newPublication.publish_delete, publication.publish_delete)
630+
assert.equal(newPublication.publish_truncate, publication.publish_truncate)
631+
assert.equal(newPublication.tables.includes('users'), true)
632+
})
633+
it('GET', async () => {
634+
const res = await axios.get(`${URL}/publications`)
635+
const newPublication = res.data[0]
636+
assert.equal(newPublication.name, publication.name)
637+
})
638+
it('PATCH', async () => {
639+
const res = await axios.get(`${URL}/publications`)
640+
const { id } = res.data[0]
641+
642+
const { data: updated } = await axios.patch(`${URL}/publications/${id}`, {
643+
name: 'b',
644+
publish_insert: false,
645+
tables: [],
646+
})
647+
assert.equal(updated.name, 'b')
648+
assert.equal(updated.publish_insert, false)
649+
assert.equal(updated.tables.includes('users'), false)
650+
})
651+
it('DELETE', async () => {
652+
const res = await axios.get(`${URL}/publications`)
653+
const { id } = res.data[0]
654+
655+
await axios.delete(`${URL}/publications/${id}`)
656+
const { data: publications } = await axios.get(`${URL}/publications`)
657+
const stillExists = publications.some((x) => x.id === id)
658+
assert.equal(stillExists, false)
659+
})
660+
})

test/postgres/mnt/init-permissions.sh

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)