Skip to content

Commit 5de5901

Browse files
committed
feat: support CRUD on /schemas, /tables, /columns
1 parent f1de031 commit 5de5901

File tree

4 files changed

+215
-41
lines changed

4 files changed

+215
-41
lines changed

src/api/columns.ts

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Router } from 'express'
2+
import SQL from 'sql-template-strings'
23
import { RunQuery } from '../lib/connectionPool'
34
import sql = require('../lib/sql')
4-
const { columns } = sql
55
import { DEFAULT_SYSTEM_SCHEMAS } from '../lib/constants'
66
import { Tables } from '../lib/interfaces'
77

88
const router = Router()
9+
const { columns, tables } = sql
10+
911
router.get('/', async (req, res) => {
1012
try {
1113
const { data } = await RunQuery(req.headers.pg, columns)
@@ -18,25 +20,87 @@ router.get('/', async (req, res) => {
1820
res.status(500).send('Database error.')
1921
}
2022
})
23+
2124
router.post('/', async (req, res) => {
2225
try {
26+
const { tableId, name, type } = req.body as {
27+
tableId: number
28+
name: string
29+
type: string
30+
}
31+
const getTableQuery = SQL``.append(tables).append(SQL` AND c.oid = ${tableId}`)
32+
const { name: table, schema } = (await RunQuery(req.headers.pg, getTableQuery)).data[0]
33+
34+
const query = `ALTER TABLE "${schema}"."${table}" ADD COLUMN "${name}" "${type}"`
35+
await RunQuery(req.headers.pg, query)
36+
37+
const getColumnQuery = SQL``
38+
.append(columns)
39+
.append(SQL` WHERE c.oid = ${tableId} AND column_name = ${name}`)
40+
const column = (await RunQuery(req.headers.pg, getColumnQuery)).data[0]
41+
42+
return res.status(200).json(column)
2343
} catch (error) {
24-
console.log('throwing error')
25-
res.status(500).send('Database error.')
44+
console.log('throwing error', error)
45+
res.status(500).json({ error: 'Database error', status: 500 })
2646
}
2747
})
48+
2849
router.patch('/:id', async (req, res) => {
2950
try {
51+
const [tableId, ordinalPos] = req.params.id.split('.')
52+
const getColumnQuery = SQL``
53+
.append(columns)
54+
.append(SQL` WHERE c.oid = ${tableId} AND ordinal_position = ${ordinalPos}`)
55+
const { schema, table, name: oldName } = (
56+
await RunQuery(req.headers.pg, getColumnQuery)
57+
).data[0]
58+
59+
const { name, type } = req.body as {
60+
name?: string
61+
type?: string
62+
}
63+
64+
const query = `
65+
BEGIN;
66+
${
67+
type === undefined
68+
? ''
69+
: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${oldName}" SET DATA TYPE "${type}";`
70+
}
71+
${
72+
name === undefined
73+
? ''
74+
: `ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${oldName}" TO "${name}";`
75+
}
76+
COMMIT;`
77+
await RunQuery(req.headers.pg, query)
78+
79+
const updated = (await RunQuery(req.headers.pg, getColumnQuery)).data[0]
80+
return res.status(200).json(updated)
3081
} catch (error) {
31-
console.log('throwing error')
32-
res.status(500).send('Database error.')
82+
console.log('throwing error', error)
83+
res.status(500).json({ error: 'Database error', status: 500 })
3384
}
3485
})
86+
3587
router.delete('/:id', async (req, res) => {
3688
try {
89+
const [tableId, ordinalPos] = req.params.id.split('.')
90+
91+
const getColumnQuery = SQL``
92+
.append(columns)
93+
.append(SQL` WHERE c.oid = ${tableId} AND ordinal_position = ${ordinalPos} `)
94+
const column = (await RunQuery(req.headers.pg, getColumnQuery)).data[0]
95+
const { schema, table, name } = column
96+
97+
const query = `ALTER TABLE "${schema}"."${table}" DROP COLUMN "${name}"`
98+
await RunQuery(req.headers.pg, query)
99+
100+
return res.status(200).json(column)
37101
} catch (error) {
38-
console.log('throwing error')
39-
res.status(500).send('Database error.')
102+
console.log('throwing error', error)
103+
res.status(500).json({ error: 'Database error', status: 500 })
40104
}
41105
})
42106

src/api/schemas.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ router.get('/', async (req, res) => {
2929
res.status(500).json({ error: 'Database error', status: 500 })
3030
}
3131
})
32+
3233
router.post('/', async (req, res) => {
3334
try {
3435
const name: string = req.body.name
@@ -37,7 +38,7 @@ router.post('/', async (req, res) => {
3738
// Create the schema
3839
const schemqQuery = createSchema(name, owner)
3940
await RunQuery(req.headers.pg, schemqQuery)
40-
41+
4142
// Return fresh details
4243
const getSchema = selectSingleByName(name)
4344
const { data } = await RunQuery(req.headers.pg, getSchema)
@@ -48,6 +49,7 @@ router.post('/', async (req, res) => {
4849
res.status(500).json({ error: 'Database error', status: 500 })
4950
}
5051
})
52+
5153
router.patch('/:id', async (req, res) => {
5254
try {
5355
const id: number = parseInt(req.params.id)
@@ -80,6 +82,23 @@ router.patch('/:id', async (req, res) => {
8082
}
8183
})
8284

85+
router.delete('/:id', async (req, res) => {
86+
try {
87+
const id = req.params.id
88+
const getNameQuery = SQL``.append(schemas).append(SQL` WHERE nsp.oid = ${id}`)
89+
const schema = (await RunQuery(req.headers.pg, getNameQuery)).data[0]
90+
91+
const cascade = req.query.cascade
92+
const query = `DROP SCHEMA "${schema.name}" ${cascade === 'true' ? 'CASCADE' : 'RESTRICT'}`
93+
await RunQuery(req.headers.pg, query)
94+
95+
return res.status(200).json(schema)
96+
} catch (error) {
97+
console.log('throwing error', error)
98+
res.status(500).json({ error: 'Database error', status: 500 })
99+
}
100+
})
101+
83102
// Helpers
84103
const selectSingleSql = (id: number) => {
85104
const query = SQL``.append(schemas).append(SQL` where nsp.oid = ${id}`)
@@ -90,7 +109,7 @@ const selectSingleByName = (name: string) => {
90109
return query
91110
}
92111
const createSchema = (name: string, owner: string = 'postgres') => {
93-
const query = SQL``.append(`CREATE SCHEMA IF NOT EXISTS ${name} AUTHORIZATION ${owner}`)
112+
const query = SQL``.append(`CREATE SCHEMA IF NOT EXISTS "${name}" AUTHORIZATION ${owner}`)
94113
return query
95114
}
96115
const alterSchemaName = (previousName: string, newName: string) => {

src/api/tables.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { DEFAULT_SYSTEM_SCHEMAS } from '../lib/constants'
88
import { Tables } from '../lib/interfaces'
99

1010
const router = Router()
11+
1112
router.get('/', async (req, res) => {
1213
try {
1314
const sql = `
1415
WITH tables AS ( ${tables} ),
15-
columns AS ( ${columns} ),
16-
grants AS ( ${grants} ),
17-
primary_keys AS ( ${primary_keys} ),
18-
relationships AS ( ${relationships} )
16+
columns AS ( ${columns} ),
17+
grants AS ( ${grants} ),
18+
primary_keys AS ( ${primary_keys} ),
19+
relationships AS ( ${relationships} )
1920
SELECT
2021
*,
2122
${coalesceRowsToArray('columns', 'SELECT * FROM columns WHERE columns.table_id = tables.id')},
@@ -46,6 +47,7 @@ FROM
4647
res.status(500).json({ error: 'Database error', status: 500 })
4748
}
4849
})
50+
4951
router.post('/', async (req, res) => {
5052
try {
5153
const { schema = 'public', name } = req.body as {
@@ -68,6 +70,7 @@ router.post('/', async (req, res) => {
6870
res.status(200).json([{ error: error.toString() }])
6971
}
7072
})
73+
7174
router.patch('/:id', async (req, res) => {
7275
try {
7376
const id: number = parseInt(req.params.id)
@@ -96,6 +99,24 @@ router.patch('/:id', async (req, res) => {
9699
}
97100
})
98101

102+
router.delete('/:id', async (req, res) => {
103+
try {
104+
const id = req.params.id
105+
const getTableQuery = SQL``.append(tables).append(SQL` AND c.oid = ${id}`)
106+
const table = (await RunQuery(req.headers.pg, getTableQuery)).data[0]
107+
const { name, schema } = table
108+
109+
const cascade = req.query.cascade
110+
const query = `DROP TABLE "${schema}"."${name}" ${cascade === 'true' ? 'CASCADE' : 'RESTRICT'}`
111+
await RunQuery(req.headers.pg, query)
112+
113+
return res.status(200).json(table)
114+
} catch (error) {
115+
console.log('throwing error', error)
116+
res.status(500).json({ error: 'Database error', status: 500 })
117+
}
118+
})
119+
99120
export = router
100121

101122
const selectSingleSql = (id: number) => {
@@ -105,11 +126,11 @@ const selectSingleByName = (schema: string, name: string) => {
105126
return SQL``.append(tables).append(SQL` and table_schema = ${schema} and table_name = ${name}`)
106127
}
107128
const createTable = (name: string, schema: string = 'postgres') => {
108-
const query = SQL``.append(`CREATE TABLE ${schema}.${name} ()`)
129+
const query = SQL``.append(`CREATE TABLE "${schema}"."${name}" ()`)
109130
return query
110131
}
111132
const alterTableName = (previousName: string, newName: string, schema: string) => {
112-
const query = SQL``.append(`ALTER SCHEMA ${previousName} RENAME TO ${newName}`)
133+
const query = SQL``.append(`ALTER TABLE "${schema}"."${previousName}" RENAME TO "${newName}"`)
113134
return query
114135
}
115136
const removeSystemSchemas = (data: Tables.Table[]) => {

test/integration/index.spec.js

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('/schemas', () => {
9999
assert.equal(true, !!datum)
100100
assert.equal(true, !!included)
101101
})
102-
it('POST & PATCH', async () => {
102+
it('POST & PATCH & DELETE', async () => {
103103
const res = await axios.post(`${URL}/schemas`, { name: 'api' })
104104
assert.equal('api', res.data.name)
105105
const newSchemaId = res.data.id
@@ -110,6 +110,15 @@ describe('/schemas', () => {
110110
owner: 'postgres',
111111
})
112112
assert.equal('api', res3.data.name)
113+
114+
const res4 = await axios.delete(`${URL}/schemas/${newSchemaId}`)
115+
assert.equal(res4.data.name, 'api')
116+
117+
const res5 = await axios.get(`${URL}/schemas`)
118+
assert.equal(
119+
res5.data.some((x) => x.id === newSchemaId),
120+
false
121+
)
113122
})
114123
})
115124
describe('/types', () => {
@@ -169,12 +178,19 @@ describe('/tables & /columns', async () => {
169178
assert.equal(true, relationship.target_table_schema === 'public')
170179
assert.equal(true, relationship.target_table_name === 'users')
171180
})
172-
it('GET /tabls with system tables', async () => {
181+
it('GET /tables with system tables', async () => {
173182
const res = await axios.get(`${URL}/tables?includeSystemSchemas=true`)
174183
const included = res.data.find((x) => `${x.schema}.${x.name}` === 'pg_catalog.pg_type')
175184
assert.equal(res.status, STATUS.SUCCESS)
176185
assert.equal(true, !!included)
177186
})
187+
// FIXME: Bad handling of query param in /tables & /columns & /schemas & /types
188+
// it('GET /tables without system tables (explicit)', async () => {
189+
// const res = await axios.get(`${URL}/tables?includeSystemSchemas=false`)
190+
// const isIncluded = res.data.some((x) => `${x.schema}.${x.name}` === 'pg_catalog.pg_type')
191+
// assert.equal(res.status, STATUS.SUCCESS)
192+
// assert.equal(isIncluded, false)
193+
// })
178194
it('GET /columns', async () => {
179195
const res = await axios.get(`${URL}/columns`)
180196
// console.log('res.data', res.data)
@@ -194,30 +210,84 @@ describe('/tables & /columns', async () => {
194210
assert.equal(true, !!included)
195211
})
196212
it('POST /tables should create a table', async () => {
197-
await axios.post(`${URL}/query`, { query: 'DROP TABLE IF EXISTS public.test' })
198-
let {data: newTable} = await axios.post(`${URL}/tables`, {
199-
schema: 'public',
200-
name: 'test',
201-
// columns: [
202-
// { name: 'id', is_identity: true, is_nullable: false, data_type: 'bigint' },
203-
// { name: 'data', data_type: 'text' },
204-
// ],
205-
// primary_keys: ['id'],
206-
})
207-
// console.log('newTable', newTable)
208-
const newTableId = newTable.id
209-
assert.equal(newTableId > 0, true)
210-
// const { data: tables } = await axios.get(`${URL}/tables`)
211-
// const test = tables.find((table) => `${table.schema}.${table.name}` === 'public.test')
212-
// const id = test.columns.find((column) => column.name === 'id')
213-
// const data = test.columns.find((column) => column.name === 'data')
214-
// assert.equal(id.is_identity, true)
215-
// assert.equal(id.is_nullable, false)
216-
// assert.equal(id.data_type, 'bigint')
217-
// assert.equal(data.is_identity, false)
218-
// assert.equal(data.is_nullable, true)
219-
// assert.equal(data.data_type, 'text')
220-
await axios.post(`${URL}/query`, { query: 'DROP TABLE public.test' })
213+
const { data: newTable } = await axios.post(`${URL}/tables`, { name: 'test' })
214+
assert.equal(`${newTable.schema}.${newTable.name}`, 'public.test')
215+
216+
const { data: tables } = await axios.get(`${URL}/tables`)
217+
assert.equal(
218+
tables.some((table) => table.id === newTable.id),
219+
true
220+
)
221+
222+
await axios.delete(`${URL}/tables/${newTable.id}`)
223+
})
224+
it('PATCH /tables', async () => {
225+
const { data: newTable } = await axios.post(`${URL}/tables`, { name: 'test' })
226+
await axios.patch(`${URL}/tables/${newTable.id}`, { name: 'test a' })
227+
const { data: tables } = await axios.get(`${URL}/tables`)
228+
assert.equal(
229+
tables.some((table) => `${table.schema}.${table.name}` === `public.test a`),
230+
true
231+
)
232+
233+
await axios.delete(`${URL}/tables/${newTable.id}`)
234+
})
235+
it('DELETE /tables', async () => {
236+
const { data: newTable } = await axios.post(`${URL}/tables`, { name: 'test' })
237+
238+
await axios.delete(`${URL}/tables/${newTable.id}`)
239+
const { data: tables } = await axios.get(`${URL}/tables`)
240+
assert.equal(
241+
tables.some((table) => `${table.schema}.${table.name}` === `public.test`),
242+
false
243+
)
244+
})
245+
it('POST /column', async () => {
246+
const { data: newTable } = await axios.post(`${URL}/tables`, { name: 'foo bar' })
247+
await axios.post(`${URL}/columns`, { tableId: newTable.id, name: 'foo bar', type: 'int2' })
248+
249+
const { data: columns } = await axios.get(`${URL}/columns`)
250+
assert.equal(
251+
columns.some(
252+
(column) =>
253+
column.id === `${newTable.id}.1` && column.name === 'foo bar' && column.format === 'int2'
254+
),
255+
true
256+
)
257+
258+
await axios.delete(`${URL}/columns/${newTable.id}.1`)
259+
await axios.delete(`${URL}/tables/${newTable.id}`)
260+
})
261+
it('PATCH /columns', async () => {
262+
const { data: newTable } = await axios.post(`${URL}/tables`, { name: 'foo bar' })
263+
await axios.post(`${URL}/columns`, { tableId: newTable.id, name: 'foo', type: 'int2' })
264+
265+
await axios.patch(`${URL}/columns/${newTable.id}.1`, { name: 'foo bar', type: 'int4' })
266+
267+
const { data: columns } = await axios.get(`${URL}/columns`)
268+
assert.equal(
269+
columns.some(
270+
(column) =>
271+
column.id === `${newTable.id}.1` && column.name === 'foo bar' && column.format === 'int4'
272+
),
273+
true
274+
)
275+
276+
await axios.delete(`${URL}/columns/${newTable.id}.1`)
277+
await axios.delete(`${URL}/tables/${newTable.id}`)
278+
})
279+
it('DELETE /columns', async () => {
280+
const { data: newTable } = await axios.post(`${URL}/tables`, { name: 'foo bar' })
281+
await axios.post(`${URL}/columns`, { tableId: newTable.id, name: 'foo bar', type: 'int2' })
282+
283+
await axios.delete(`${URL}/columns/${newTable.id}.1`)
284+
const { data: columns } = await axios.get(`${URL}/columns`)
285+
assert.equal(
286+
columns.some((column) => column.id === `${newTable.id}.1`),
287+
false
288+
)
289+
290+
await axios.delete(`${URL}/tables/${newTable.id}`)
221291
})
222292
})
223293
describe('/extensions', () => {

0 commit comments

Comments
 (0)