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
46 changes: 46 additions & 0 deletions example_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,52 @@
"unique": false
}
]
},
{
"name": "posts",
"fields": [
{
"name": "id",
"type": "integer",
"primaryKey": true
},
{
"name": "title",
"type": "string",
"nullable": false
},
{
"name": "body",
"type": "text",
"nullable": true
},
{
"name": "user_id",
"type": "integer",
"nullable": false
},
{
"name": "created_at",
"type": "datetime"
}
],
"indexes": [
{
"name": "posts_user_id_idx",
"columns": ["user_id"],
"unique": false
}
],
"foreignKeys": [
{
"name": "fk_posts_user_id",
"columns": ["user_id"],
"referenceTable": "users",
"referenceColumns": ["id"],
"onDelete": "CASCADE",
"onUpdate": "CASCADE"
}
]
}
]
}
74 changes: 74 additions & 0 deletions src/database/fk-creator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { DatabaseQuery, ModelConfig, DBEngine } from '../types';

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

function validateIdentifier(value: string, label: string) {
if (!VALID_IDENTIFIER.test(value)) {
throw new Error(`Invalid ${label}: ${value}`);
}
}

export async function createForeignKeys(
db: DatabaseQuery,
models: ModelConfig[],
engine: DBEngine,
logger: { info: (msg: string) => void; warn: (msg: string) => void }
) {
for (const model of models) {
if (!model.foreignKeys || model.foreignKeys.length === 0) continue;

for (const fk of model.foreignKeys) {
validateIdentifier(fk.name, 'foreign key name');
validateIdentifier(model.name, 'table name');
validateIdentifier(fk.referenceTable, 'reference table name');
for (const col of fk.columns) {
validateIdentifier(col, `column name in FK "${fk.name}"`);
}
for (const col of fk.referenceColumns) {
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]
);
fkExists = res[0].exists;
} else {
// SQLite doesn't support ALTER TABLE ADD CONSTRAINT for FKs.
// FKs must be defined at table creation time in SQLite.
logger.warn(
`SQLite does not support adding foreign keys via ALTER TABLE. FK "${fk.name}" on "${model.name}" skipped.`
);
continue;
}

if (fkExists) {
logger.info(`Foreign key "${fk.name}" on table "${model.name}" already exists, skipping.`);
continue;
}

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})`;

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

sql += ';';

logger.info(
`Creating foreign key "${fk.name}" on "${model.name}" referencing "${fk.referenceTable}" (${fk.referenceColumns.join(', ')})...`
);
await db.query(sql);
logger.info(`Foreign key "${fk.name}" created successfully.`);
}
}
}
4 changes: 3 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AppConfig, Mode } from './types';
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) {
if (swaggerConfig.enabled) {
Expand Down Expand Up @@ -79,10 +80,11 @@ export async function startServer(config: AppConfig, port: number, mode: Mode) {
connection: config.database.connection,
});

// Auto-generate tables and indexes if models are provided
// Auto-generate tables, indexes, and foreign keys if models are provided
if (config.models && config.models.length > 0) {
await createTables(app.db, config.models, config.database.engine, app.log);
await createIndexes(app.db, config.models, config.database.engine, app.log);
await createForeignKeys(app.db, config.models, config.database.engine, app.log);
}

// register swagger
Expand Down
11 changes: 11 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 Down Expand Up @@ -62,10 +63,20 @@ export interface ModelIndex {
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 {
Expand Down
Loading