Skip to content
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
4 changes: 2 additions & 2 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', ['feat', 'chore', 'ci', 'bug']],
'type-enum': [2, 'always', ['feat', 'chore', 'ci', 'bug', 'refactor']],
'header-max-length': [2, 'always', 120],
'subject-case': [0], // Disable to allow JIRA key
'subject-empty': [2, 'never'],
Expand All @@ -15,7 +15,7 @@ module.exports = {
rules: {
'header-match-jira': ({ header }) => {
// type(scope): ROSS-NUMBER: message
const regex = /^(feat|chore|ci|bug)\([a-z-]+\): ROSS-\d+: .+/;
const regex = /^(feat|chore|ci|bug|refactor)\([a-z-]+\): ROSS-\d+: .+/;
return [
regex.test(header),
'Commit message must follow the convention: type(scope): ROSS-<NUMBER>: message\nTypes: feat, chore, ci, bug',
Expand Down
9 changes: 6 additions & 3 deletions example_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,21 @@
{
"name": "id",
"type": "integer",
"primaryKey": true
"primaryKey": true,
"supportedOperation": ["editable", "deletable"]
},
{
"name": "email",
"type": "string",
"unique": true,
"nullable": false
"nullable": false,
"supportedOperation": ["searchable"]
},
{
"name": "name",
"type": "string",
"nullable": true
"nullable": true,
"supportedOperation": ["searchable"]
},
{
"name": "is_active",
Expand Down
26 changes: 15 additions & 11 deletions src/database/fk-creator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { DatabaseQuery, ModelConfig, DBEngine } from '../types';
import SQL from 'sql-template-strings';

import { DatabaseQuery } from '../types';
import { ModelConfig, DBEngine } from '../schema/config';

const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;

Expand Down Expand Up @@ -28,13 +31,10 @@ export async function createForeignKeys(
validateIdentifier(col, `reference column name in FK "${fk.name}"`);
}

// Check if constraint already exists
let fkExists = false;
if (engine === 'pg') {
const res = await db.query<string, { exists: boolean }>(
"SELECT EXISTS (SELECT FROM information_schema.table_constraints WHERE constraint_name = $1 AND constraint_type = 'FOREIGN KEY')",
[fk.name]
);
const query = SQL`SELECT EXISTS (SELECT FROM information_schema.table_constraints WHERE constraint_name = ${fk.name} AND constraint_type = 'FOREIGN KEY')`;
const res = await db.query<string, { exists: boolean }>(query.sql, query.values);
fkExists = res[0].exists;
} else {
// SQLite doesn't support ALTER TABLE ADD CONSTRAINT for FKs.
Expand All @@ -53,21 +53,25 @@ export async function createForeignKeys(
const columnList = fk.columns.map((c) => `"${c}"`).join(', ');
const refColumnList = fk.referenceColumns.map((c) => `"${c}"`).join(', ');

let sql = `ALTER TABLE "${model.name}" ADD CONSTRAINT "${fk.name}" FOREIGN KEY (${columnList}) REFERENCES "${fk.referenceTable}" (${refColumnList})`;
const statement = SQL`ALTER TABLE `
.append(`"${model.name}"`)
.append(` ADD CONSTRAINT "${fk.name}" FOREIGN KEY (${columnList}) REFERENCES `)
.append(`"${fk.referenceTable}"`)
.append(` (${refColumnList})`);

if (fk.onDelete) {
sql += ` ON DELETE ${fk.onDelete}`;
statement.append(` ON DELETE ${fk.onDelete}`);
}
if (fk.onUpdate) {
sql += ` ON UPDATE ${fk.onUpdate}`;
statement.append(` ON UPDATE ${fk.onUpdate}`);
}

sql += ';';
statement.append(';');

logger.info(
`Creating foreign key "${fk.name}" on "${model.name}" referencing "${fk.referenceTable}" (${fk.referenceColumns.join(', ')})...`
);
await db.query(sql);
await db.query(statement.sql, statement.values);
logger.info(`Foreign key "${fk.name}" created successfully.`);
}
}
Expand Down
31 changes: 19 additions & 12 deletions src/database/index-creator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { DatabaseQuery, ModelConfig, DBEngine } from '../types';
import SQL from 'sql-template-strings';

import { DatabaseQuery } from '../types';
import { ModelConfig, DBEngine } from '../schema/config';

export async function createIndexes(
db: DatabaseQuery,
Expand All @@ -19,23 +22,27 @@ export async function createIndexes(
}
}

const uniqueKeyword = index.unique ? 'UNIQUE ' : '';
const columnList = index.columns.map((c) => `"${c}"`).join(', ');
const sql = `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS "${index.name}" ON "${model.name}" (${columnList});`;
const statement = SQL`CREATE `;
if (index.unique) {
statement.append('UNIQUE ');
}
statement
.append('INDEX IF NOT EXISTS ')
.append(`"${index.name}"`)
.append(' ON ')
.append(`"${model.name}"`)
.append(` (${columnList});`);

// Check if index already exists
let indexExists = false;
if (engine === 'pg') {
const res = await db.query<string, { exists: boolean }>(
"SELECT EXISTS (SELECT FROM pg_indexes WHERE schemaname = 'public' AND indexname = $1)",
[index.name]
);
const query = SQL`SELECT EXISTS (SELECT FROM pg_indexes WHERE schemaname = 'public' AND indexname = ${index.name})`;
const res = await db.query<string, { exists: boolean }>(query.sql, query.values);
indexExists = res[0].exists;
} else {
const res = await db.query<string, { count: number }>(
"SELECT count(*) as count FROM sqlite_master WHERE type='index' AND name=$1",
[index.name]
);
const query = SQL`SELECT count(*) as count FROM sqlite_master WHERE type='index' AND name=${index.name}`;
const res = await db.query<string, { count: number }>(query.sql, query.values);
indexExists = res[0].count > 0;
}

Expand All @@ -47,7 +54,7 @@ export async function createIndexes(
logger.info(
`Creating ${index.unique ? 'unique ' : ''}index "${index.name}" on table "${model.name}" (${index.columns.join(', ')})...`
);
await db.query(sql);
await db.query(statement.sql, statement.values);
logger.info(`Index "${index.name}" created successfully.`);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/database/table-creator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import SQL from 'sql-template-strings';
import { DatabaseQuery, ModelConfig, DBEngine } from '../types';

import { DatabaseQuery } from '../types';
import { ModelConfig, DBEngine } from '../schema/config';

export async function createTables(
db: DatabaseQuery,
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import chalk from 'chalk';
import { Command } from 'commander';
import * as fs from 'fs';

import { CLIOptions, AppConfig } from './types';
import { CLIOptions } from './types';
import { AppConfig } from './schema/config';
import { validateConfigPath, validateMode, validatePort } from './validators';
import { startServer } from './server';

Expand Down
4 changes: 3 additions & 1 deletion src/plugin/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import fp from 'fastify-plugin';
import { FastifyInstance } from 'fastify';
import { Pool } from 'pg';
import Database from 'better-sqlite3';
import { DatabaseConfig, DatabaseQuery } from '../types';

import { DatabaseQuery } from '../types';
import { DatabaseConfig } from '../schema/config';

export default fp(async (fastify: FastifyInstance, opts: DatabaseConfig) => {
let db: DatabaseQuery;
Expand Down
81 changes: 81 additions & 0 deletions src/schema/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export type DBEngine = 'sqlite' | 'pg';
export type DataType = 'integer' | 'string' | 'boolean' | 'text' | 'datetime';
export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
export type SupportedOperations =
| 'searchable'
| 'sortable'
| 'editable'
| 'deletable'
| 'lessThan'
| 'lessThanEqual'
| 'greaterThan'
| 'greaterThanEqual'
| 'equal'
| 'oneOf';
export type SupportedAggregationOperation = 'mean' | 'max' | 'min' | 'count' | 'sum' | 'frequency';

export interface SwaggerConfig {
enabled: boolean;
basePath: string;
info: {
title: string;
description: string;
version: string;
termsOfService?: string;
contact?: {
name?: string;
url?: string;
email?: string;
};
license?: {
name: string;
url?: string;
};
};
}

export interface DatabaseConfig {
engine: DBEngine;
connection: {
urlOrPath: string;
};
}

export interface ModelFieldConfig {
name: string;
type: DataType;
primaryKey?: boolean;
nullable?: boolean;
unique?: boolean;
default?: unknown;
supportedOperations?: SupportedOperations[];
supportedAggregation?: SupportedAggregationOperation[];
}

export interface ModelIndexConfig {
name: string;
columns: string[];
unique?: boolean;
}

export interface ModelForeignKeyConfig {
name: string;
columns: string[];
referenceTable: string;
referenceColumns: string[];
onDelete?: ForeignKeyAction;
onUpdate?: ForeignKeyAction;
}

export interface ModelConfig {
name: string;
fields: ModelFieldConfig[];
indexes?: ModelIndexConfig[];
foreignKeys?: ModelForeignKeyConfig[];
}

export interface AppConfig {
swagger: SwaggerConfig;
database: DatabaseConfig;
models: ModelConfig[];
}
5 changes: 3 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import Fastify, { FastifyInstance, FastifyRequest, FastifyReply, FastifyError }
import swagger from '@fastify/swagger';
import swaggerUI from '@fastify/swagger-ui';

import { AppConfig, Mode } from './types';
import { Mode } from './types';
import { AppConfig, SwaggerConfig } from './schema/config';
import dbPlugin from './plugin/database';
import { createTables } from './database/table-creator';
import { createIndexes } from './database/index-creator';
import { createForeignKeys } from './database/fk-creator';

async function registerSwagger(swaggerConfig: AppConfig['swagger'], app: FastifyInstance) {
async function registerSwagger(swaggerConfig: SwaggerConfig, app: FastifyInstance) {
if (swaggerConfig.enabled) {
// Swagger (OpenAPI spec)
await app.register(swagger, {
Expand Down
67 changes: 0 additions & 67 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
export type Mode = 'dev' | 'prod';
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
export type DBEngine = 'sqlite' | 'pg';
export type DataType = 'integer' | 'string' | 'boolean' | 'text' | 'datetime';
export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';

export interface CLIOptions {
config: string;
Expand All @@ -16,71 +13,7 @@ export interface CLIOptions {
mode: Mode;
}

export interface SwaggerConfig {
enabled: boolean;
basePath: string;
info: {
title: string;
description: string;
version: string;
termsOfService?: string;
contact?: {
name?: string;
url?: string;
email?: string;
};
license?: {
name: string;
url?: string;
};
};
}

export interface DatabaseConfig {
engine: DBEngine;
connection: {
urlOrPath: string;
};
}

export interface DatabaseQuery {
query<T, Q>(sql: string, params?: T[]): Promise<Q[]>;
close: () => Promise<void>;
}

export interface ModelField {
name: string;
type: DataType;
primaryKey?: boolean;
nullable?: boolean;
unique?: boolean;
default?: unknown;
}

export interface ModelIndex {
name: string;
columns: string[];
unique?: boolean;
}

export interface ModelForeignKey {
name: string;
columns: string[];
referenceTable: string;
referenceColumns: string[];
onDelete?: ForeignKeyAction;
onUpdate?: ForeignKeyAction;
}

export interface ModelConfig {
name: string;
fields: ModelField[];
indexes?: ModelIndex[];
foreignKeys?: ModelForeignKey[];
}

export interface AppConfig {
swagger: SwaggerConfig;
database: DatabaseConfig;
models: ModelConfig[];
}
Loading