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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
rules: {
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "off"
},
};

2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default [
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
{
Expand All @@ -30,6 +31,7 @@ export default [
rules: {
"no-undef": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
];
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"jsonwebtoken": "8.5.1",
"nanoid": "3",
"pg": "^8.13.3",
"pg-format": "^1.0.4",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.5",
"uuid": "^11.0.5"
Expand Down
1 change: 1 addition & 0 deletions src/config/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ const sequelize = new Sequelize(
},
);

import '../models/relations';
export { sequelize };
4 changes: 2 additions & 2 deletions src/controllers/ddl-controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Request, Response } from 'express';
import { sequelize } from '../config/database';
import { successResponse, errorResponse } from '../utils/response';
import { Operations } from '../types/ddl';
import { DDLOperations } from '../types/ddl';
import { DDLExecutor } from '../operations/migrate';

export const migrate = async (req: Request, res: Response) => {
const { operations }: { operations: Operations[] } = req.body;
const { operations }: { operations: DDLOperations[] } = req.body;
if (!operations || !Array.isArray(operations))
return errorResponse(res, 'Invalid payload structure', 400);

Expand Down
26 changes: 26 additions & 0 deletions src/controllers/dml-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Request, Response } from 'express';
import { sequelize } from '../config/database';
import { successResponse, errorResponse } from '../utils/response';
import { DMLOperations } from '../types/dml';
import { DMLExecutor } from '../operations/execute';

export const execute = async (req: Request, res: Response) => {
const { operations }: { operations: DMLOperations[] } = req.body;
if (!operations || !Array.isArray(operations))
return errorResponse(res, 'Invalid payload structure', 400);

const transaction = await sequelize.transaction();

try {
const result = await DMLExecutor.execute(operations, transaction);
await transaction.commit();
return successResponse(
res,
result,
'DML operations completed successfully',
);
} catch (error: any) {
await transaction.rollback();
return errorResponse(res, error.message, 500);
}
};
14 changes: 14 additions & 0 deletions src/controllers/schema-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Request, Response } from 'express';
import SchemaRepository from '../repositories/schema-repository';

export const schema = async (req: Request, res: Response) => {
try {
const tables = await SchemaRepository.getSchemas();

return res.json({ success: true, tables });
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ error: errorMessage });
}
};
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import userRoutes from './routes/user-routes';
import authRoutes from './routes/auth-routes';
import ddlRoutes from './routes/ddl-routes';
import { sequelize } from './config/database';
import schemaRoutes from './routes/schema-routes';
import dmlRoutes from './routes/dml-routes';

sequelize
.sync({ force: true })
.sync({ alter: true })
.then(async () => {
console.log('Database synchronized successfully.');
})
Expand All @@ -26,6 +28,8 @@ const apiRouter = express.Router();
apiRouter.use('/auth', authRoutes);
apiRouter.use('/users', userRoutes);
apiRouter.use('/migrate', ddlRoutes);
apiRouter.use('/schemas', schemaRoutes);
apiRouter.use('/execute', dmlRoutes);

app.use('/api', apiRouter);

Expand Down
5 changes: 0 additions & 5 deletions src/models/metadata-column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,4 @@ MetadataColumn.init(
},
);

MetadataColumn.belongsTo(MetadataTable, {
foreignKey: 'table_id',
onDelete: 'CASCADE',
});

export default MetadataColumn;
1 change: 0 additions & 1 deletion src/models/metadata-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ class MetadataTable extends Model {
public table_name!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
primaryKey: any;
}

MetadataTable.init(
Expand Down
14 changes: 14 additions & 0 deletions src/models/relations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import MetadataTable from './metadata-table';
import MetadataColumn from './metadata-column';

MetadataTable.hasMany(MetadataColumn, {
foreignKey: 'table_id',
as: 'columns',
onDelete: 'CASCADE',
});

MetadataColumn.belongsTo(MetadataTable, {
foreignKey: 'table_id',
as: 'table',
onDelete: 'CASCADE',
});
1 change: 1 addition & 0 deletions src/operations/ddl/create-column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class CreateColumn {
ALTER TABLE "${table}" ADD COLUMN "${columnName}" ${colType}
${columnDefinition.nullable ? '' : 'NOT NULL'}
${columnDefinition.unique ? 'UNIQUE' : ''}
${colType === 'timestamp' ? 'DEFAULT NOW()' : ''}
`;

await sequelize.query(addColumnQuery, { transaction });
Expand Down
48 changes: 48 additions & 0 deletions src/operations/dml/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Transaction } from 'sequelize';
import { DeleteInstruction } from '../../types/dml';
import { DMLRepository } from '../../repositories/dml-repository';
import MetadataTableRepository from '../../repositories/metadata-table-repository';
import MetadataColumnRepository from '../../repositories/metadata-column-repository';
import {
parseAndValidateCondition,
validIdentifier,
} from '../../utils/validation';

export class DeleteOperation {
static async execute(
instruction: DeleteInstruction,
transaction: Transaction,
) {
const { table, condition, params } = instruction;

if (!validIdentifier(table))
throw new Error(`Invalid table name: ${table}`);

const metadataTable = await MetadataTableRepository.findOne(
{ table_name: table },
transaction,
);
if (!metadataTable) throw new Error(`Table ${table} does not exist`);

const metadataColumns = await MetadataColumnRepository.findAll(
{ table_id: metadataTable.id },
transaction,
);

const parsedCondition = condition
? parseAndValidateCondition(condition, metadataColumns)
: {};
const result = await DMLRepository.delete(
table,
parsedCondition,
params,
transaction,
);

await transaction.afterCommit(() => {
console.log(`Data deleted from ${table} successfully`);
});

return result;
}
}
4 changes: 4 additions & 0 deletions src/operations/dml/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { SelectOperation } from './select';
export { InsertOperation } from './insert';
export { UpdateOperation } from './update';
export { DeleteOperation } from './delete';
30 changes: 30 additions & 0 deletions src/operations/dml/insert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Transaction } from 'sequelize';
import { InsertInstruction } from '../../types/dml';
import { DMLRepository } from '../../repositories/dml-repository';
import { parseAndValidateData, validIdentifier } from '../../utils/validation';

export class InsertOperation {
static async execute(
instruction: InsertInstruction,
transaction: Transaction,
) {
const { table, data } = instruction;

if (!validIdentifier(table)) {
throw new Error(`Invalid table name: ${table}`);
}

if (!data || Object.keys(data).length === 0) {
throw new Error('Insert data cannot be empty');
}

const parsedData = await parseAndValidateData(table, data, transaction);
const result = await DMLRepository.insert(table, parsedData, transaction);

await transaction.afterCommit(() => {
console.log(`Data inserted into ${table} successfully`);
});

return result[0][0].id;
}
}
43 changes: 43 additions & 0 deletions src/operations/dml/select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Transaction } from 'sequelize';
import { SelectInstruction } from '../../types/dml';
import { DMLRepository } from '../../repositories/dml-repository';
import MetadataTableRepository from '../../repositories/metadata-table-repository';
import {
parseAndValidateCondition,
validIdentifier,
} from '../../utils/validation';

export class SelectOperation {
static async execute(
instruction: SelectInstruction,
transaction: Transaction,
) {
const { table, condition, orderBy, limit, offset, params } = instruction;

if (!validIdentifier(table))
throw new Error(`Invalid table name: ${table}`);

const metadataTable = await MetadataTableRepository.findOne(
{ table_name: table },
transaction,
);
if (!metadataTable) {
throw new Error(`Table ${table} does not exist`);
}

const parsedCondition = condition
? parseAndValidateCondition(condition)
: {};
const result = await DMLRepository.select(
table,
parsedCondition,
orderBy,
limit,
offset,
params,
transaction,
);

return result;
}
}
42 changes: 42 additions & 0 deletions src/operations/dml/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Transaction } from 'sequelize';
import { UpdateInstruction } from '../../types/dml';
import { DMLRepository } from '../../repositories/dml-repository';
import {
parseAndValidateCondition,
parseAndValidateData,
validIdentifier,
} from '../../utils/validation';

export class UpdateOperation {
static async execute(
instruction: UpdateInstruction,
transaction: Transaction,
) {
const { table, condition, set, params } = instruction;

if (!validIdentifier(table))
throw new Error(`Invalid table name: ${table}`);

if (!set || Object.keys(set).length === 0)
throw new Error('Update set cannot be empty');

const parsedSet = await parseAndValidateData(table, set, transaction);
const parsedCondition = condition
? parseAndValidateCondition(condition)
: {};

const result = await DMLRepository.update(
table,
parsedSet,
parsedCondition,
params,
transaction,
);

await transaction.afterCommit(() => {
console.log(`Data updated in ${table} successfully`);
});

return result;
}
}
58 changes: 58 additions & 0 deletions src/operations/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Transaction } from 'sequelize';
import { DMLOperations } from '../types/dml';
import {
SelectOperation,
InsertOperation,
UpdateOperation,
DeleteOperation,
} from '../operations/dml';

export class DMLExecutor {
static async execute(operations: DMLOperations[], transaction: Transaction) {
const results: Record<string, any>[] = [];

for (const { operation, instruction } of operations) {
switch (operation) {
case 'Select': {
const selectResult = await SelectOperation.execute(
instruction,
transaction,
);
results.push(selectResult);
break;
}
case 'Insert': {
const insertResult = await InsertOperation.execute(
instruction,
transaction,
);
results.push(insertResult);
break;
}
case 'Update': {
const updateResult = await UpdateOperation.execute(
instruction,
transaction,
);
if (updateResult) {
results.push(updateResult);
}
break;
}
case 'Delete': {
const deleteResult = await DeleteOperation.execute(
instruction,
transaction,
);
results.push(deleteResult);
break;
}
default: {
throw new Error(`Unsupported operation: ${operation}`);
}
}
}

return results;
}
}
Loading