Skip to content

feat: support CRUD on /schemas, /tables, /columns #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 130 additions & 3 deletions src/api/columns.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,145 @@
import { Router } from 'express'

import SQL from 'sql-template-strings'
import { RunQuery } from '../lib/connectionPool'
import sql = require('../lib/sql')
const { columns } = sql
import { DEFAULT_SYSTEM_SCHEMAS } from '../lib/constants'
import { Tables } from '../lib/interfaces'

const router = Router()
const { columns, tables } = sql

router.get('/', async (req, res) => {
try {
const { data } = await RunQuery(req.headers.pg, columns)
return res.status(200).json(data)
const query: Fetch.QueryParams = req.query
let payload: Tables.Column[] = data
if (!query?.includeSystemSchemas) payload = removeSystemSchemas(data)
return res.status(200).json(payload)
} catch (error) {
console.log('throwing error')
res.status(500).send('Database error.')
}
})

router.post('/', async (req, res) => {
try {
const { tableId, name, type } = req.body as {
tableId: number
name: string
type: string
}
const getTableQuery = SQL``.append(tables).append(SQL` AND c.oid = ${tableId}`)
const { name: table, schema } = (await RunQuery(req.headers.pg, getTableQuery)).data[0]

const query = `ALTER TABLE "${schema}"."${table}" ADD COLUMN "${name}" "${type}"`
await RunQuery(req.headers.pg, query)

const getColumnQuery = SQL``
.append(columns)
.append(SQL` WHERE c.oid = ${tableId} AND column_name = ${name}`)
const column = (await RunQuery(req.headers.pg, getColumnQuery)).data[0]

return res.status(200).json(column)
} catch (error) {
console.log('throwing error', error)
res.status(500).json({ error: 'Database error', status: 500 })
}
})

router.patch('/:id', async (req, res) => {
try {
const [tableId, ordinalPos] = req.params.id.split('.')
const getColumnQuery = SQL``
.append(columns)
.append(SQL` WHERE c.oid = ${tableId} AND ordinal_position = ${ordinalPos}`)
const { schema, table, name: oldName } = (
await RunQuery(req.headers.pg, getColumnQuery)
).data[0]

const { name, type } = req.body as {
name?: string
type?: string
}

const query = `
BEGIN;
${
type === undefined
? ''
: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${oldName}" SET DATA TYPE "${type}";`
}
${
name === undefined
? ''
: `ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${oldName}" TO "${name}";`
}
COMMIT;`
await RunQuery(req.headers.pg, query)

const updated = (await RunQuery(req.headers.pg, getColumnQuery)).data[0]
return res.status(200).json(updated)
} catch (error) {
console.log('throwing error', error)
res.status(500).json({ error: 'Database error', status: 500 })
}
})

router.delete('/:id', async (req, res) => {
try {
const [tableId, ordinalPos] = req.params.id.split('.')

const getColumnQuery = SQL``
.append(columns)
.append(SQL` WHERE c.oid = ${tableId} AND ordinal_position = ${ordinalPos} `)
const column = (await RunQuery(req.headers.pg, getColumnQuery)).data[0]
const { schema, table, name } = column

const query = `ALTER TABLE "${schema}"."${table}" DROP COLUMN "${name}"`
await RunQuery(req.headers.pg, query)

return res.status(200).json(column)
} catch (error) {
console.log('throwing error', error)
res.status(500).json({ error: 'Database error', status: 500 })
}
})

export = router

const removeSystemSchemas = (data: Tables.Column[]) => {
return data.filter((x) => !DEFAULT_SYSTEM_SCHEMAS.includes(x.schema))
}
const newColumnSql = ({
name,
default_value,
is_identity = false,
is_nullable = true,
is_primary_key = false,
data_type,
}: {
name: string
default_value?: string
is_identity?: boolean
is_nullable?: boolean
is_primary_key?: boolean
data_type: string
}) => {
return `
${name} ${data_type}
${default_value === undefined ? '' : `DEFAULT ${default_value}`}
${is_identity ? 'GENERATED BY DEFAULT AS IDENTITY' : ''}
${is_nullable ? '' : 'NOT NULL'}
${is_primary_key ? 'PRIMARY KEY' : ''}`
}

/**
* Types
*/
namespace Fetch {
/**
* @param {boolean} [includeSystemSchemas=false] - Return system schemas as well as user schemas
*/
export interface QueryParams {
includeSystemSchemas?: boolean
}
}
30 changes: 25 additions & 5 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ router.get('/', async (req, res) => {
res.status(500).json({ error: 'Database error', status: 500 })
}
})

router.post('/', async (req, res) => {
try {
const name: string = req.body.name
Expand All @@ -37,7 +38,7 @@ router.post('/', async (req, res) => {
// Create the schema
const schemqQuery = createSchema(name, owner)
await RunQuery(req.headers.pg, schemqQuery)

// Return fresh details
const getSchema = selectSingleByName(name)
const { data } = await RunQuery(req.headers.pg, getSchema)
Expand All @@ -48,6 +49,7 @@ router.post('/', async (req, res) => {
res.status(500).json({ error: 'Database error', status: 500 })
}
})

router.patch('/:id', async (req, res) => {
try {
const id: number = parseInt(req.params.id)
Expand All @@ -64,15 +66,33 @@ router.patch('/:id', async (req, res) => {
const updateOwner = alterSchemaOwner(previousSchema.name, owner)
await RunQuery(req.headers.pg, updateOwner)
}
// NB: Run name updates last
if (name) {
const updateName = alterSchemaName(previousSchema.name, name)
await RunQuery(req.headers.pg, updateName)
}

// Return fresh details
const { data: updatedSchemaResults } = await RunQuery(req.headers.pg, getSchema)
let updatedSchema: Schemas.Schema = updatedSchemaResults[0]
return res.status(200).json(updatedSchema)
const { data: updatedResults } = await RunQuery(req.headers.pg, getSchema)
let updated: Schemas.Schema = updatedResults[0]
return res.status(200).json(updated)
} catch (error) {
console.log('throwing error', error)
res.status(500).json({ error: 'Database error', status: 500 })
}
})

router.delete('/:id', async (req, res) => {
try {
const id = req.params.id
const getNameQuery = SQL``.append(schemas).append(SQL` WHERE nsp.oid = ${id}`)
const schema = (await RunQuery(req.headers.pg, getNameQuery)).data[0]

const cascade = req.query.cascade
const query = `DROP SCHEMA "${schema.name}" ${cascade === 'true' ? 'CASCADE' : 'RESTRICT'}`
await RunQuery(req.headers.pg, query)

return res.status(200).json(schema)
} catch (error) {
console.log('throwing error', error)
res.status(500).json({ error: 'Database error', status: 500 })
Expand All @@ -89,7 +109,7 @@ const selectSingleByName = (name: string) => {
return query
}
const createSchema = (name: string, owner: string = 'postgres') => {
const query = SQL``.append(`CREATE SCHEMA IF NOT EXISTS ${name} AUTHORIZATION ${owner}`)
const query = SQL``.append(`CREATE SCHEMA IF NOT EXISTS "${name}" AUTHORIZATION ${owner}`)
return query
}
const alterSchemaName = (previousName: string, newName: string) => {
Expand Down
101 changes: 82 additions & 19 deletions src/api/tables.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { Router } from 'express'

import sql = require('../lib/sql')
const { columns, grants, policies, primary_keys, relationships, tables } = sql
import { coalesceRowsToArray, formatColumns } from '../lib/helpers'
import SQL from 'sql-template-strings'
import sqlTemplates = require('../lib/sql')
const { columns, grants, policies, primary_keys, relationships, tables } = sqlTemplates
import { coalesceRowsToArray } from '../lib/helpers'
import { RunQuery } from '../lib/connectionPool'
import { DEFAULT_SYSTEM_SCHEMAS } from '../lib/constants'
import { Tables } from '../lib/interfaces'

const router = Router()

router.get('/', async (req, res) => {
try {
const sql = `
WITH tables AS ( ${tables} ),
columns AS ( ${columns} ),
grants AS ( ${grants} ),
primary_keys AS ( ${primary_keys} ),
policies AS ( ${policies} ),
relationships AS ( ${relationships} )
columns AS ( ${columns} ),
grants AS ( ${grants} ),
policies AS ( ${policies} ),
primary_keys AS ( ${primary_keys} ),
relationships AS ( ${relationships} )
SELECT
*,
${coalesceRowsToArray('columns', 'SELECT * FROM columns WHERE columns.table_id = tables.id')},
Expand Down Expand Up @@ -48,37 +49,99 @@ FROM
res.status(500).json({ error: 'Database error', status: 500 })
}
})

router.post('/', async (req, res) => {
try {
const { schema = 'public', name, columns, primary_keys = [] } = req.body as {
const { schema = 'public', name } = req.body as {
schema?: string
name: string
columns: Tables.Column[]
primary_keys?: Tables.PrimaryKey[]
}
const sql = `
CREATE TABLE ${schema}.${name} (
${formatColumns({ columns, primary_keys })}
)`
const { data } = await RunQuery(req.headers.pg, sql)
return res.status(200).json(data)

// Create the table
const createTableSql = createTable(name, schema)
await RunQuery(req.headers.pg, createTableSql)

// Return fresh details
const getTable = selectSingleByName(schema, name)
const { data: newTableResults } = await RunQuery(req.headers.pg, getTable)
let newTable: Tables.Table = newTableResults[0]
return res.status(200).json(newTable)
} catch (error) {
// For this one, we always want to give back the error to the customer
console.log('Soft error!', error)
res.status(200).json([{ error: error.toString() }])
}
})

router.patch('/:id', async (req, res) => {
try {
const id: number = parseInt(req.params.id)
const name: string = req.body.name

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

// Update fields
// NB: Run name updates last
if (name) {
const updateName = alterTableName(previousTable.name, name, previousTable.schema)
await RunQuery(req.headers.pg, updateName)
}

// Return fresh details
const { data: updatedResults } = await RunQuery(req.headers.pg, getTableSql)
let updated: Tables.Table = updatedResults[0]
return res.status(200).json(updated)
} catch (error) {
// For this one, we always want to give back the error to the customer
console.log('Soft error!', error)
res.status(200).json([{ error: error.toString() }])
}
})

router.delete('/:id', async (req, res) => {
try {
const id = req.params.id
const getTableQuery = SQL``.append(tables).append(SQL` AND c.oid = ${id}`)
const table = (await RunQuery(req.headers.pg, getTableQuery)).data[0]
const { name, schema } = table

const cascade = req.query.cascade
const query = `DROP TABLE "${schema}"."${name}" ${cascade === 'true' ? 'CASCADE' : 'RESTRICT'}`
await RunQuery(req.headers.pg, query)

return res.status(200).json(table)
} catch (error) {
console.log('throwing error', error)
res.status(500).json({ error: 'Database error', status: 500 })
}
})

export = router

const selectSingleSql = (id: number) => {
return SQL``.append(tables).append(SQL` and c.oid = ${id}`)
}
const selectSingleByName = (schema: string, name: string) => {
return SQL``.append(tables).append(SQL` and table_schema = ${schema} and table_name = ${name}`)
}
const createTable = (name: string, schema: string = 'postgres') => {
const query = SQL``.append(`CREATE TABLE "${schema}"."${name}" ()`)
return query
}
const alterTableName = (previousName: string, newName: string, schema: string) => {
const query = SQL``.append(`ALTER TABLE "${schema}"."${previousName}" RENAME TO "${newName}"`)
return query
}
const removeSystemSchemas = (data: Tables.Table[]) => {
return data.filter((x) => !DEFAULT_SYSTEM_SCHEMAS.includes(x.schema))
}

/**
* Types
*/

namespace Fetch {
/**
* @param {boolean} [includeSystemSchemas=false] - Return system schemas as well as user schemas
Expand Down
Loading