Skip to content

Commit 1f71af3

Browse files
authored
Merge pull request #28 from supabase/feature/modify_tables
feat: support CRUD on /schemas, /tables, /columns
2 parents d2fb929 + 18a156f commit 1f71af3

File tree

10 files changed

+363
-96
lines changed

10 files changed

+363
-96
lines changed

src/api/columns.ts

+130-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,145 @@
11
import { Router } from 'express'
2-
2+
import SQL from 'sql-template-strings'
33
import { RunQuery } from '../lib/connectionPool'
44
import sql = require('../lib/sql')
5-
const { columns } = sql
5+
import { DEFAULT_SYSTEM_SCHEMAS } from '../lib/constants'
6+
import { Tables } from '../lib/interfaces'
67

78
const router = Router()
9+
const { columns, tables } = sql
10+
811
router.get('/', async (req, res) => {
912
try {
1013
const { data } = await RunQuery(req.headers.pg, columns)
11-
return res.status(200).json(data)
14+
const query: Fetch.QueryParams = req.query
15+
let payload: Tables.Column[] = data
16+
if (!query?.includeSystemSchemas) payload = removeSystemSchemas(data)
17+
return res.status(200).json(payload)
1218
} catch (error) {
1319
console.log('throwing error')
1420
res.status(500).send('Database error.')
1521
}
1622
})
1723

24+
router.post('/', async (req, res) => {
25+
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)
43+
} catch (error) {
44+
console.log('throwing error', error)
45+
res.status(500).json({ error: 'Database error', status: 500 })
46+
}
47+
})
48+
49+
router.patch('/:id', async (req, res) => {
50+
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)
81+
} catch (error) {
82+
console.log('throwing error', error)
83+
res.status(500).json({ error: 'Database error', status: 500 })
84+
}
85+
})
86+
87+
router.delete('/:id', async (req, res) => {
88+
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)
101+
} catch (error) {
102+
console.log('throwing error', error)
103+
res.status(500).json({ error: 'Database error', status: 500 })
104+
}
105+
})
106+
18107
export = router
108+
109+
const removeSystemSchemas = (data: Tables.Column[]) => {
110+
return data.filter((x) => !DEFAULT_SYSTEM_SCHEMAS.includes(x.schema))
111+
}
112+
const newColumnSql = ({
113+
name,
114+
default_value,
115+
is_identity = false,
116+
is_nullable = true,
117+
is_primary_key = false,
118+
data_type,
119+
}: {
120+
name: string
121+
default_value?: string
122+
is_identity?: boolean
123+
is_nullable?: boolean
124+
is_primary_key?: boolean
125+
data_type: string
126+
}) => {
127+
return `
128+
${name} ${data_type}
129+
${default_value === undefined ? '' : `DEFAULT ${default_value}`}
130+
${is_identity ? 'GENERATED BY DEFAULT AS IDENTITY' : ''}
131+
${is_nullable ? '' : 'NOT NULL'}
132+
${is_primary_key ? 'PRIMARY KEY' : ''}`
133+
}
134+
135+
/**
136+
* Types
137+
*/
138+
namespace Fetch {
139+
/**
140+
* @param {boolean} [includeSystemSchemas=false] - Return system schemas as well as user schemas
141+
*/
142+
export interface QueryParams {
143+
includeSystemSchemas?: boolean
144+
}
145+
}

src/api/schemas.ts

+25-5
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)
@@ -64,15 +66,33 @@ router.patch('/:id', async (req, res) => {
6466
const updateOwner = alterSchemaOwner(previousSchema.name, owner)
6567
await RunQuery(req.headers.pg, updateOwner)
6668
}
69+
// NB: Run name updates last
6770
if (name) {
6871
const updateName = alterSchemaName(previousSchema.name, name)
6972
await RunQuery(req.headers.pg, updateName)
7073
}
7174

7275
// Return fresh details
73-
const { data: updatedSchemaResults } = await RunQuery(req.headers.pg, getSchema)
74-
let updatedSchema: Schemas.Schema = updatedSchemaResults[0]
75-
return res.status(200).json(updatedSchema)
76+
const { data: updatedResults } = await RunQuery(req.headers.pg, getSchema)
77+
let updated: Schemas.Schema = updatedResults[0]
78+
return res.status(200).json(updated)
79+
} catch (error) {
80+
console.log('throwing error', error)
81+
res.status(500).json({ error: 'Database error', status: 500 })
82+
}
83+
})
84+
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)
7696
} catch (error) {
7797
console.log('throwing error', error)
7898
res.status(500).json({ error: 'Database error', status: 500 })
@@ -89,7 +109,7 @@ const selectSingleByName = (name: string) => {
89109
return query
90110
}
91111
const createSchema = (name: string, owner: string = 'postgres') => {
92-
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}`)
93113
return query
94114
}
95115
const alterSchemaName = (previousName: string, newName: string) => {

src/api/tables.ts

+82-19
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import { Router } from 'express'
2-
3-
import sql = require('../lib/sql')
4-
const { columns, grants, policies, primary_keys, relationships, tables } = sql
5-
import { coalesceRowsToArray, formatColumns } from '../lib/helpers'
2+
import SQL from 'sql-template-strings'
3+
import sqlTemplates = require('../lib/sql')
4+
const { columns, grants, policies, primary_keys, relationships, tables } = sqlTemplates
5+
import { coalesceRowsToArray } from '../lib/helpers'
66
import { RunQuery } from '../lib/connectionPool'
77
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-
policies AS ( ${policies} ),
19-
relationships AS ( ${relationships} )
16+
columns AS ( ${columns} ),
17+
grants AS ( ${grants} ),
18+
policies AS ( ${policies} ),
19+
primary_keys AS ( ${primary_keys} ),
20+
relationships AS ( ${relationships} )
2021
SELECT
2122
*,
2223
${coalesceRowsToArray('columns', 'SELECT * FROM columns WHERE columns.table_id = tables.id')},
@@ -48,37 +49,99 @@ FROM
4849
res.status(500).json({ error: 'Database error', status: 500 })
4950
}
5051
})
52+
5153
router.post('/', async (req, res) => {
5254
try {
53-
const { schema = 'public', name, columns, primary_keys = [] } = req.body as {
55+
const { schema = 'public', name } = req.body as {
5456
schema?: string
5557
name: string
56-
columns: Tables.Column[]
57-
primary_keys?: Tables.PrimaryKey[]
5858
}
59-
const sql = `
60-
CREATE TABLE ${schema}.${name} (
61-
${formatColumns({ columns, primary_keys })}
62-
)`
63-
const { data } = await RunQuery(req.headers.pg, sql)
64-
return res.status(200).json(data)
59+
60+
// Create the table
61+
const createTableSql = createTable(name, schema)
62+
await RunQuery(req.headers.pg, createTableSql)
63+
64+
// Return fresh details
65+
const getTable = selectSingleByName(schema, name)
66+
const { data: newTableResults } = await RunQuery(req.headers.pg, getTable)
67+
let newTable: Tables.Table = newTableResults[0]
68+
return res.status(200).json(newTable)
6569
} catch (error) {
6670
// For this one, we always want to give back the error to the customer
6771
console.log('Soft error!', error)
6872
res.status(200).json([{ error: error.toString() }])
6973
}
7074
})
7175

76+
router.patch('/:id', async (req, res) => {
77+
try {
78+
const id: number = parseInt(req.params.id)
79+
const name: string = req.body.name
80+
81+
// Get table
82+
const getTableSql = selectSingleSql(id)
83+
const { data: getTableResults } = await RunQuery(req.headers.pg, getTableSql)
84+
let previousTable: Tables.Table = getTableResults[0]
85+
86+
// Update fields
87+
// NB: Run name updates last
88+
if (name) {
89+
const updateName = alterTableName(previousTable.name, name, previousTable.schema)
90+
await RunQuery(req.headers.pg, updateName)
91+
}
92+
93+
// Return fresh details
94+
const { data: updatedResults } = await RunQuery(req.headers.pg, getTableSql)
95+
let updated: Tables.Table = updatedResults[0]
96+
return res.status(200).json(updated)
97+
} catch (error) {
98+
// For this one, we always want to give back the error to the customer
99+
console.log('Soft error!', error)
100+
res.status(200).json([{ error: error.toString() }])
101+
}
102+
})
103+
104+
router.delete('/:id', async (req, res) => {
105+
try {
106+
const id = req.params.id
107+
const getTableQuery = SQL``.append(tables).append(SQL` AND c.oid = ${id}`)
108+
const table = (await RunQuery(req.headers.pg, getTableQuery)).data[0]
109+
const { name, schema } = table
110+
111+
const cascade = req.query.cascade
112+
const query = `DROP TABLE "${schema}"."${name}" ${cascade === 'true' ? 'CASCADE' : 'RESTRICT'}`
113+
await RunQuery(req.headers.pg, query)
114+
115+
return res.status(200).json(table)
116+
} catch (error) {
117+
console.log('throwing error', error)
118+
res.status(500).json({ error: 'Database error', status: 500 })
119+
}
120+
})
121+
72122
export = router
73123

124+
const selectSingleSql = (id: number) => {
125+
return SQL``.append(tables).append(SQL` and c.oid = ${id}`)
126+
}
127+
const selectSingleByName = (schema: string, name: string) => {
128+
return SQL``.append(tables).append(SQL` and table_schema = ${schema} and table_name = ${name}`)
129+
}
130+
const createTable = (name: string, schema: string = 'postgres') => {
131+
const query = SQL``.append(`CREATE TABLE "${schema}"."${name}" ()`)
132+
return query
133+
}
134+
const alterTableName = (previousName: string, newName: string, schema: string) => {
135+
const query = SQL``.append(`ALTER TABLE "${schema}"."${previousName}" RENAME TO "${newName}"`)
136+
return query
137+
}
74138
const removeSystemSchemas = (data: Tables.Table[]) => {
75139
return data.filter((x) => !DEFAULT_SYSTEM_SCHEMAS.includes(x.schema))
76140
}
77141

78142
/**
79143
* Types
80144
*/
81-
82145
namespace Fetch {
83146
/**
84147
* @param {boolean} [includeSystemSchemas=false] - Return system schemas as well as user schemas

0 commit comments

Comments
 (0)