diff --git a/src/lib/types.ts b/src/lib/types.ts index 82e89294..0e55b35a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -43,6 +43,45 @@ export const postgresColumnSchema = Type.Object({ }) export type PostgresColumn = Static +export const postgresColumnCreateSchema = Type.Object({ + table_id: Type.Integer(), + name: Type.String(), + type: Type.String(), + default_value: Type.Optional(Type.Unknown()), + default_value_format: Type.Optional( + Type.Union([Type.Literal('expression'), Type.Literal('literal')]) + ), + is_identity: Type.Optional(Type.Boolean()), + identity_generation: Type.Optional( + Type.Union([Type.Literal('BY DEFAULT'), Type.Literal('ALWAYS')]) + ), + is_nullable: Type.Optional(Type.Boolean()), + is_primary_key: Type.Optional(Type.Boolean()), + is_unique: Type.Optional(Type.Boolean()), + comment: Type.Optional(Type.String()), + check: Type.Optional(Type.String()), +}) +export type PostgresColumnCreate = Static + +export const postgresColumnUpdateSchema = Type.Object({ + name: Type.Optional(Type.String()), + type: Type.Optional(Type.String()), + drop_default: Type.Optional(Type.Boolean()), + default_value: Type.Optional(Type.Unknown()), + default_value_format: Type.Optional( + Type.Union([Type.Literal('expression'), Type.Literal('literal')]) + ), + is_identity: Type.Optional(Type.Boolean()), + identity_generation: Type.Optional( + Type.Union([Type.Literal('BY DEFAULT'), Type.Literal('ALWAYS')]) + ), + is_nullable: Type.Optional(Type.Boolean()), + is_unique: Type.Optional(Type.Boolean()), + comment: Type.Optional(Type.String()), + check: Type.Optional(Type.Union([Type.String(), Type.Null()])), +}) +export type PostgresColumnUpdate = Static + // TODO Rethink config.sql export const postgresConfigSchema = Type.Object({ name: Type.Unknown(), diff --git a/src/server/routes/columns.ts b/src/server/routes/columns.ts index e861a569..54a7ba2a 100644 --- a/src/server/routes/columns.ts +++ b/src/server/routes/columns.ts @@ -1,71 +1,52 @@ -import { FastifyInstance } from 'fastify' import { PostgresMeta } from '../../lib/index.js' import { DEFAULT_POOL_CONFIG } from '../constants.js' import { extractRequestForLogging } from '../utils.js' +import { Type } from '@sinclair/typebox' +import { + postgresColumnCreateSchema, + postgresColumnSchema, + postgresColumnUpdateSchema, +} from '../../lib/types.js' +import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' + +const route: FastifyPluginAsyncTypebox = async (fastify) => { + fastify.get( + '/', + { + schema: { + headers: Type.Object({ + pg: Type.String(), + }), + querystring: Type.Object({ + include_system_schemas: Type.Optional(Type.Boolean()), + included_schemas: Type.Optional(Type.String()), + excluded_schemas: Type.Optional(Type.String()), + limit: Type.Optional(Type.Integer()), + offset: Type.Optional(Type.Integer()), + }), + response: { + 200: Type.Array(postgresColumnSchema), + 500: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + const includeSystemSchemas = request.query.include_system_schemas + const includedSchemas = request.query.included_schemas?.split(',') + const excludedSchemas = request.query.excluded_schemas?.split(',') + const limit = request.query.limit + const offset = request.query.offset -export default async (fastify: FastifyInstance) => { - fastify.get<{ - Headers: { pg: string } - Querystring: { - include_system_schemas?: string - // Note: this only supports comma separated values (e.g., ".../columns?included_schemas=public,core") - included_schemas?: string - excluded_schemas?: string - limit?: number - offset?: number - } - }>('/', async (request, reply) => { - const connectionString = request.headers.pg - const includeSystemSchemas = request.query.include_system_schemas === 'true' - const includedSchemas = request.query.included_schemas?.split(',') - const excludedSchemas = request.query.excluded_schemas?.split(',') - const limit = request.query.limit - const offset = request.query.offset - - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data, error } = await pgMeta.columns.list({ - includeSystemSchemas, - includedSchemas, - excludedSchemas, - limit, - offset, - }) - await pgMeta.end() - if (error) { - request.log.error({ error, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: error.message } - } - - return data - }) - - fastify.get<{ - Headers: { pg: string } - Params: { - tableId: string - ordinalPosition: string - } - Querystring: { - include_system_schemas?: string - limit?: string - offset?: string - } - }>('/:tableId(^\\d+):ordinalPosition', async (request, reply) => { - if (request.params.ordinalPosition === '') { - const { - headers: { pg: connectionString }, - query: { limit, offset }, - params: { tableId }, - } = request - const includeSystemSchemas = request.query.include_system_schemas === 'true' - - const pgMeta: PostgresMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) const { data, error } = await pgMeta.columns.list({ - tableId: Number(tableId), includeSystemSchemas, - limit: Number(limit), - offset: Number(offset), + includedSchemas, + excludedSchemas, + limit, + offset, }) await pgMeta.end() if (error) { @@ -75,15 +56,104 @@ export default async (fastify: FastifyInstance) => { } return data - } else if (/^\.\d+$/.test(request.params.ordinalPosition)) { - const { - headers: { pg: connectionString }, - params: { tableId, ordinalPosition: ordinalPositionWithDot }, - } = request - const ordinalPosition = ordinalPositionWithDot.slice(1) + } + ) + + fastify.get( + '/:tableId(^\\d+):ordinalPosition', + { + schema: { + headers: Type.Object({ + pg: Type.String(), + }), + params: Type.Object({ + tableId: Type.String(), + ordinalPosition: Type.String(), + }), + querystring: Type.Object({ + include_system_schemas: Type.Optional(Type.Boolean()), + limit: Type.Optional(Type.Integer()), + offset: Type.Optional(Type.Integer()), + }), + response: { + 200: postgresColumnSchema, + 400: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + if (request.params.ordinalPosition === '') { + const { + headers: { pg: connectionString }, + query: { limit, offset }, + params: { tableId }, + } = request + const includeSystemSchemas = request.query.include_system_schemas + + const pgMeta: PostgresMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columns.list({ + tableId: Number(tableId), + includeSystemSchemas, + limit: Number(limit), + offset: Number(offset), + }) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(500) + return { error: error.message } + } + + return data[0] + } else if (/^\.\d+$/.test(request.params.ordinalPosition)) { + const { + headers: { pg: connectionString }, + params: { tableId, ordinalPosition: ordinalPositionWithDot }, + } = request + const ordinalPosition = ordinalPositionWithDot.slice(1) + + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columns.retrieve({ + id: `${tableId}.${ordinalPosition}`, + }) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } + + return data + } else { + return reply.callNotFound() + } + } + ) + + fastify.post( + '/', + { + schema: { + headers: Type.Object({ + pg: Type.String(), + }), + body: postgresColumnCreateSchema, + response: { + 200: postgresColumnSchema, + 400: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data, error } = await pgMeta.columns.retrieve({ id: `${tableId}.${ordinalPosition}` }) + const { data, error } = await pgMeta.columns.create(request.body) await pgMeta.end() if (error) { request.log.error({ error, request: extractRequestForLogging(request) }) @@ -93,74 +163,83 @@ export default async (fastify: FastifyInstance) => { } return data - } else { - return reply.callNotFound() - } - }) - - fastify.post<{ - Headers: { pg: string } - Body: any - }>('/', async (request, reply) => { - const connectionString = request.headers.pg - - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data, error } = await pgMeta.columns.create(request.body as any) - await pgMeta.end() - if (error) { - request.log.error({ error, request: extractRequestForLogging(request) }) - reply.code(400) - if (error.message.startsWith('Cannot find')) reply.code(404) - return { error: error.message } } + ) + + fastify.patch( + '/:id(\\d+\\.\\d+)', + { + schema: { + headers: Type.Object({ + pg: Type.String(), + }), + params: Type.Object({ + id: Type.String(), + }), + body: postgresColumnUpdateSchema, + response: { + 200: postgresColumnSchema, + 400: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg - return data - }) + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columns.update(request.params.id, request.body) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } - fastify.patch<{ - Headers: { pg: string } - Params: { - id: string - } - Body: any - }>('/:id(\\d+\\.\\d+)', async (request, reply) => { - const connectionString = request.headers.pg - - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data, error } = await pgMeta.columns.update(request.params.id, request.body as any) - await pgMeta.end() - if (error) { - request.log.error({ error, request: extractRequestForLogging(request) }) - reply.code(400) - if (error.message.startsWith('Cannot find')) reply.code(404) - return { error: error.message } + return data } + ) + + fastify.delete( + '/:id(\\d+\\.\\d+)', + { + schema: { + headers: Type.Object({ + pg: Type.String(), + }), + params: Type.Object({ + id: Type.String(), + }), + querystring: Type.Object({ + cascade: Type.Optional(Type.String()), + }), + response: { + 200: postgresColumnSchema, + 400: Type.Object({ + error: Type.String(), + }), + }, + }, + }, + async (request, reply) => { + const connectionString = request.headers.pg + const cascade = request.query.cascade === 'true' - return data - }) + const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data, error } = await pgMeta.columns.remove(request.params.id, { cascade }) + await pgMeta.end() + if (error) { + request.log.error({ error, request: extractRequestForLogging(request) }) + reply.code(400) + if (error.message.startsWith('Cannot find')) reply.code(404) + return { error: error.message } + } - fastify.delete<{ - Headers: { pg: string } - Params: { - id: string - } - Querystring: { - cascade?: string - } - }>('/:id(\\d+\\.\\d+)', async (request, reply) => { - const connectionString = request.headers.pg - const cascade = request.query.cascade === 'true' - - const pgMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data, error } = await pgMeta.columns.remove(request.params.id, { cascade }) - await pgMeta.end() - if (error) { - request.log.error({ error, request: extractRequestForLogging(request) }) - reply.code(400) - if (error.message.startsWith('Cannot find')) reply.code(404) - return { error: error.message } + return data } - - return data - }) + ) } + +export default route