From 2f520112f1763e929670172054584dbeb79d2c80 Mon Sep 17 00:00:00 2001 From: Abhishek Chatterjee Date: Sun, 29 Mar 2026 23:05:37 +0530 Subject: [PATCH 1/3] refactor(model): ROSS-11: move configuration schemas to a dedicated file and update server imports --- commitlint.config.js | 4 +-- src/database/fk-creator.ts | 3 +- src/database/index-creator.ts | 3 +- src/database/table-creator.ts | 4 ++- src/main.ts | 3 +- src/plugin/database.ts | 4 ++- src/schema/config.ts | 67 +++++++++++++++++++++++++++++++++++ src/server.ts | 5 +-- src/types/index.ts | 67 ----------------------------------- 9 files changed, 84 insertions(+), 76 deletions(-) create mode 100644 src/schema/config.ts diff --git a/commitlint.config.js b/commitlint.config.js index 2f1c703..af0cc84 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -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'], @@ -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-: message\nTypes: feat, chore, ci, bug', diff --git a/src/database/fk-creator.ts b/src/database/fk-creator.ts index 8909f66..2d9d6db 100644 --- a/src/database/fk-creator.ts +++ b/src/database/fk-creator.ts @@ -1,4 +1,5 @@ -import { DatabaseQuery, ModelConfig, DBEngine } from '../types'; +import { DatabaseQuery } from '../types'; +import { ModelConfig, DBEngine } from '../schema/config'; const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/; diff --git a/src/database/index-creator.ts b/src/database/index-creator.ts index 04487b1..664908e 100644 --- a/src/database/index-creator.ts +++ b/src/database/index-creator.ts @@ -1,4 +1,5 @@ -import { DatabaseQuery, ModelConfig, DBEngine } from '../types'; +import { DatabaseQuery } from '../types'; +import { ModelConfig, DBEngine } from '../schema/config'; export async function createIndexes( db: DatabaseQuery, diff --git a/src/database/table-creator.ts b/src/database/table-creator.ts index 9ae7562..2b1624e 100644 --- a/src/database/table-creator.ts +++ b/src/database/table-creator.ts @@ -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, diff --git a/src/main.ts b/src/main.ts index c139cc4..45617f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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'; diff --git a/src/plugin/database.ts b/src/plugin/database.ts index c0c991e..c260d2b 100644 --- a/src/plugin/database.ts +++ b/src/plugin/database.ts @@ -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; diff --git a/src/schema/config.ts b/src/schema/config.ts new file mode 100644 index 0000000..a6bc414 --- /dev/null +++ b/src/schema/config.ts @@ -0,0 +1,67 @@ +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 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; +} + +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[]; +} diff --git a/src/server.ts b/src/server.ts index 09cc0ff..4d2938c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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, { diff --git a/src/types/index.ts b/src/types/index.ts index 440b953..92ad490 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; @@ -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(sql: string, params?: T[]): Promise; close: () => Promise; } - -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[]; -} From e2d8d9b5a454a74ed864fe374f7d75f5b98a48ef Mon Sep 17 00:00:00 2001 From: Abhishek Chatterjee Date: Sun, 29 Mar 2026 23:08:00 +0530 Subject: [PATCH 2/3] refactor(model): ROSS-11: migrate database schema creation queries to use sql-template-strings for improved security --- src/database/fk-creator.ts | 23 +++++++++++++---------- src/database/index-creator.ts | 28 +++++++++++++++++----------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/database/fk-creator.ts b/src/database/fk-creator.ts index 2d9d6db..5fe7b2a 100644 --- a/src/database/fk-creator.ts +++ b/src/database/fk-creator.ts @@ -1,3 +1,5 @@ +import SQL from 'sql-template-strings'; + import { DatabaseQuery } from '../types'; import { ModelConfig, DBEngine } from '../schema/config'; @@ -29,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( - "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(query.sql, query.values); fkExists = res[0].exists; } else { // SQLite doesn't support ALTER TABLE ADD CONSTRAINT for FKs. @@ -54,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.`); } } diff --git a/src/database/index-creator.ts b/src/database/index-creator.ts index 664908e..ea9894b 100644 --- a/src/database/index-creator.ts +++ b/src/database/index-creator.ts @@ -1,3 +1,5 @@ +import SQL from 'sql-template-strings'; + import { DatabaseQuery } from '../types'; import { ModelConfig, DBEngine } from '../schema/config'; @@ -20,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( - "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(query.sql, query.values); indexExists = res[0].exists; } else { - const res = await db.query( - "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(query.sql, query.values); indexExists = res[0].count > 0; } @@ -48,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.`); } } From 7b5178e1cede081881caad6a5dfd29b9e6289292 Mon Sep 17 00:00:00 2001 From: Abhishek Chatterjee Date: Sun, 29 Mar 2026 23:15:01 +0530 Subject: [PATCH 3/3] feat(model): ROSS-11: add supportedOperations and supportedAggregation fields to schema configuration --- example_config.json | 9 ++++++--- src/schema/config.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/example_config.json b/example_config.json index c3073e0..7a31da0 100644 --- a/example_config.json +++ b/example_config.json @@ -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", diff --git a/src/schema/config.ts b/src/schema/config.ts index a6bc414..081b0c6 100644 --- a/src/schema/config.ts +++ b/src/schema/config.ts @@ -1,6 +1,18 @@ 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; @@ -36,6 +48,8 @@ export interface ModelFieldConfig { nullable?: boolean; unique?: boolean; default?: unknown; + supportedOperations?: SupportedOperations[]; + supportedAggregation?: SupportedAggregationOperation[]; } export interface ModelIndexConfig {