Added auth functionality, initial work with views and field types
This commit is contained in:
216
backend/src/object/schema-management.service.ts
Normal file
216
backend/src/object/schema-management.service.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { ObjectDefinition } from '../models/object-definition.model';
|
||||
import { FieldDefinition } from '../models/field-definition.model';
|
||||
|
||||
@Injectable()
|
||||
export class SchemaManagementService {
|
||||
private readonly logger = new Logger(SchemaManagementService.name);
|
||||
|
||||
/**
|
||||
* Create a physical table for an object definition
|
||||
*/
|
||||
async createObjectTable(
|
||||
knex: Knex,
|
||||
objectDefinition: ObjectDefinition,
|
||||
fields: FieldDefinition[],
|
||||
) {
|
||||
const tableName = this.getTableName(objectDefinition.apiName);
|
||||
|
||||
// Check if table already exists
|
||||
const exists = await knex.schema.hasTable(tableName);
|
||||
if (exists) {
|
||||
throw new Error(`Table ${tableName} already exists`);
|
||||
}
|
||||
|
||||
await knex.schema.createTable(tableName, (table) => {
|
||||
// Standard fields
|
||||
table.uuid('id').primary().defaultTo(knex.raw('(UUID())'));
|
||||
table.timestamps(true, true);
|
||||
|
||||
// Custom fields from field definitions
|
||||
for (const field of fields) {
|
||||
this.addFieldColumn(table, field);
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log(`Created table: ${tableName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new field to an existing object table
|
||||
*/
|
||||
async addFieldToTable(
|
||||
knex: Knex,
|
||||
objectApiName: string,
|
||||
field: FieldDefinition,
|
||||
) {
|
||||
const tableName = this.getTableName(objectApiName);
|
||||
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
this.addFieldColumn(table, field);
|
||||
});
|
||||
|
||||
this.logger.log(`Added field ${field.apiName} to table ${tableName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a field from an existing object table
|
||||
*/
|
||||
async removeFieldFromTable(
|
||||
knex: Knex,
|
||||
objectApiName: string,
|
||||
fieldApiName: string,
|
||||
) {
|
||||
const tableName = this.getTableName(objectApiName);
|
||||
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
table.dropColumn(fieldApiName);
|
||||
});
|
||||
|
||||
this.logger.log(`Removed field ${fieldApiName} from table ${tableName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop an object table
|
||||
*/
|
||||
async dropObjectTable(knex: Knex, objectApiName: string) {
|
||||
const tableName = this.getTableName(objectApiName);
|
||||
|
||||
await knex.schema.dropTableIfExists(tableName);
|
||||
|
||||
this.logger.log(`Dropped table: ${tableName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field column to a table builder
|
||||
*/
|
||||
private addFieldColumn(
|
||||
table: Knex.CreateTableBuilder | Knex.AlterTableBuilder,
|
||||
field: FieldDefinition,
|
||||
) {
|
||||
const columnName = field.apiName;
|
||||
|
||||
let column: Knex.ColumnBuilder;
|
||||
|
||||
switch (field.type) {
|
||||
case 'String':
|
||||
column = table.string(columnName, field.length || 255);
|
||||
break;
|
||||
|
||||
case 'Text':
|
||||
column = table.text(columnName);
|
||||
break;
|
||||
|
||||
case 'Number':
|
||||
if (field.scale && field.scale > 0) {
|
||||
column = table.decimal(
|
||||
columnName,
|
||||
field.precision || 10,
|
||||
field.scale,
|
||||
);
|
||||
} else {
|
||||
column = table.integer(columnName);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Boolean':
|
||||
column = table.boolean(columnName).defaultTo(false);
|
||||
break;
|
||||
|
||||
case 'Date':
|
||||
column = table.date(columnName);
|
||||
break;
|
||||
|
||||
case 'DateTime':
|
||||
column = table.datetime(columnName);
|
||||
break;
|
||||
|
||||
case 'Reference':
|
||||
column = table.uuid(columnName);
|
||||
if (field.referenceObject) {
|
||||
const refTableName = this.getTableName(field.referenceObject);
|
||||
column.references('id').inTable(refTableName).onDelete('SET NULL');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Email':
|
||||
column = table.string(columnName, 255);
|
||||
break;
|
||||
|
||||
case 'Phone':
|
||||
column = table.string(columnName, 50);
|
||||
break;
|
||||
|
||||
case 'Url':
|
||||
column = table.string(columnName, 255);
|
||||
break;
|
||||
|
||||
case 'Json':
|
||||
column = table.json(columnName);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported field type: ${field.type}`);
|
||||
}
|
||||
|
||||
if (field.isRequired) {
|
||||
column.notNullable();
|
||||
} else {
|
||||
column.nullable();
|
||||
}
|
||||
|
||||
if (field.isUnique) {
|
||||
column.unique();
|
||||
}
|
||||
|
||||
if (field.defaultValue) {
|
||||
column.defaultTo(field.defaultValue);
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object API name to table name (convert to snake_case, pluralize)
|
||||
*/
|
||||
private getTableName(apiName: string): string {
|
||||
// Convert PascalCase to snake_case
|
||||
const snakeCase = apiName
|
||||
.replace(/([A-Z])/g, '_$1')
|
||||
.toLowerCase()
|
||||
.replace(/^_/, '');
|
||||
|
||||
// Simple pluralization (append 's' if not already plural)
|
||||
// In production, use a proper pluralization library
|
||||
return snakeCase.endsWith('s') ? snakeCase : `${snakeCase}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate field definition before creating column
|
||||
*/
|
||||
validateFieldDefinition(field: FieldDefinition) {
|
||||
if (!field.apiName || !field.label || !field.type) {
|
||||
throw new Error('Field must have apiName, label, and type');
|
||||
}
|
||||
|
||||
// Validate field name (alphanumeric + underscore, starts with letter)
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(field.apiName)) {
|
||||
throw new Error(`Invalid field name: ${field.apiName}`);
|
||||
}
|
||||
|
||||
// Validate reference field has referenceObject
|
||||
if (field.type === 'Reference' && !field.referenceObject) {
|
||||
throw new Error('Reference field must specify referenceObject');
|
||||
}
|
||||
|
||||
// Validate numeric fields
|
||||
if (field.type === 'Number') {
|
||||
if (field.scale && field.scale > 0 && !field.precision) {
|
||||
throw new Error('Decimal fields must specify precision');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user