Add Contact standard object, related lists, meilisearch, pagination, search, AI assistant
This commit is contained in:
@@ -15,7 +15,11 @@ export class SchemaManagementService {
|
||||
objectDefinition: ObjectDefinition,
|
||||
fields: FieldDefinition[],
|
||||
) {
|
||||
const tableName = this.getTableName(objectDefinition.apiName);
|
||||
const tableName = this.getTableName(
|
||||
objectDefinition.apiName,
|
||||
objectDefinition.label,
|
||||
objectDefinition.pluralLabel,
|
||||
);
|
||||
|
||||
// Check if table already exists
|
||||
const exists = await knex.schema.hasTable(tableName);
|
||||
@@ -44,8 +48,10 @@ export class SchemaManagementService {
|
||||
knex: Knex,
|
||||
objectApiName: string,
|
||||
field: FieldDefinition,
|
||||
objectLabel?: string,
|
||||
pluralLabel?: string,
|
||||
) {
|
||||
const tableName = this.getTableName(objectApiName);
|
||||
const tableName = this.getTableName(objectApiName, objectLabel, pluralLabel);
|
||||
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
this.addFieldColumn(table, field);
|
||||
@@ -61,8 +67,10 @@ export class SchemaManagementService {
|
||||
knex: Knex,
|
||||
objectApiName: string,
|
||||
fieldApiName: string,
|
||||
objectLabel?: string,
|
||||
pluralLabel?: string,
|
||||
) {
|
||||
const tableName = this.getTableName(objectApiName);
|
||||
const tableName = this.getTableName(objectApiName, objectLabel, pluralLabel);
|
||||
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
table.dropColumn(fieldApiName);
|
||||
@@ -71,11 +79,44 @@ export class SchemaManagementService {
|
||||
this.logger.log(`Removed field ${fieldApiName} from table ${tableName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter a field in an existing object table
|
||||
* Handles safe updates like changing NOT NULL or constraints
|
||||
* Warns about potentially destructive operations
|
||||
*/
|
||||
async alterFieldInTable(
|
||||
knex: Knex,
|
||||
objectApiName: string,
|
||||
fieldApiName: string,
|
||||
field: FieldDefinition,
|
||||
objectLabel?: string,
|
||||
pluralLabel?: string,
|
||||
options?: {
|
||||
skipTypeChange?: boolean; // Skip if type change would lose data
|
||||
},
|
||||
) {
|
||||
const tableName = this.getTableName(objectApiName, objectLabel, pluralLabel);
|
||||
const skipTypeChange = options?.skipTypeChange ?? true;
|
||||
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
// Drop the existing column and recreate with new definition
|
||||
// Note: This approach works for metadata changes, but type changes may need data migration
|
||||
table.dropColumn(fieldApiName);
|
||||
});
|
||||
|
||||
// Recreate the column with new definition
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
this.addFieldColumn(table, field);
|
||||
});
|
||||
|
||||
this.logger.log(`Altered field ${fieldApiName} in table ${tableName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop an object table
|
||||
*/
|
||||
async dropObjectTable(knex: Knex, objectApiName: string) {
|
||||
const tableName = this.getTableName(objectApiName);
|
||||
async dropObjectTable(knex: Knex, objectApiName: string, objectLabel?: string, pluralLabel?: string) {
|
||||
const tableName = this.getTableName(objectApiName, objectLabel, pluralLabel);
|
||||
|
||||
await knex.schema.dropTableIfExists(tableName);
|
||||
|
||||
@@ -94,15 +135,30 @@ export class SchemaManagementService {
|
||||
let column: Knex.ColumnBuilder;
|
||||
|
||||
switch (field.type) {
|
||||
// Text types
|
||||
case 'String':
|
||||
case 'TEXT':
|
||||
case 'EMAIL':
|
||||
case 'PHONE':
|
||||
case 'URL':
|
||||
column = table.string(columnName, field.length || 255);
|
||||
break;
|
||||
|
||||
case 'Text':
|
||||
case 'LONG_TEXT':
|
||||
column = table.text(columnName);
|
||||
break;
|
||||
|
||||
case 'PICKLIST':
|
||||
case 'MULTI_PICKLIST':
|
||||
column = table.string(columnName, 255);
|
||||
break;
|
||||
|
||||
// Numeric types
|
||||
case 'Number':
|
||||
case 'NUMBER':
|
||||
case 'CURRENCY':
|
||||
case 'PERCENT':
|
||||
if (field.scale && field.scale > 0) {
|
||||
column = table.decimal(
|
||||
columnName,
|
||||
@@ -115,18 +171,28 @@ export class SchemaManagementService {
|
||||
break;
|
||||
|
||||
case 'Boolean':
|
||||
case 'BOOLEAN':
|
||||
column = table.boolean(columnName).defaultTo(false);
|
||||
break;
|
||||
|
||||
// Date types
|
||||
case 'Date':
|
||||
case 'DATE':
|
||||
column = table.date(columnName);
|
||||
break;
|
||||
|
||||
case 'DateTime':
|
||||
case 'DATE_TIME':
|
||||
column = table.datetime(columnName);
|
||||
break;
|
||||
|
||||
case 'TIME':
|
||||
column = table.time(columnName);
|
||||
break;
|
||||
|
||||
// Relationship types
|
||||
case 'Reference':
|
||||
case 'LOOKUP':
|
||||
column = table.uuid(columnName);
|
||||
if (field.referenceObject) {
|
||||
const refTableName = this.getTableName(field.referenceObject);
|
||||
@@ -134,19 +200,30 @@ export class SchemaManagementService {
|
||||
}
|
||||
break;
|
||||
|
||||
// Email (legacy)
|
||||
case 'Email':
|
||||
column = table.string(columnName, 255);
|
||||
break;
|
||||
|
||||
// Phone (legacy)
|
||||
case 'Phone':
|
||||
column = table.string(columnName, 50);
|
||||
break;
|
||||
|
||||
// Url (legacy)
|
||||
case 'Url':
|
||||
column = table.string(columnName, 255);
|
||||
break;
|
||||
|
||||
// File types
|
||||
case 'FILE':
|
||||
case 'IMAGE':
|
||||
column = table.text(columnName); // Store file path or URL
|
||||
break;
|
||||
|
||||
// JSON
|
||||
case 'Json':
|
||||
case 'JSON':
|
||||
column = table.json(columnName);
|
||||
break;
|
||||
|
||||
@@ -174,16 +251,35 @@ export class SchemaManagementService {
|
||||
/**
|
||||
* 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(/^_/, '');
|
||||
private getTableName(apiName: string, objectLabel?: string, pluralLabel?: string): string {
|
||||
const toSnakePlural = (source: string): string => {
|
||||
const cleaned = source.replace(/[\s-]+/g, '_');
|
||||
const snake = cleaned
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||
.replace(/__+/g, '_')
|
||||
.toLowerCase()
|
||||
.replace(/^_/, '');
|
||||
|
||||
// Simple pluralization (append 's' if not already plural)
|
||||
// In production, use a proper pluralization library
|
||||
return snakeCase.endsWith('s') ? snakeCase : `${snakeCase}s`;
|
||||
if (snake.endsWith('y')) return `${snake.slice(0, -1)}ies`;
|
||||
if (snake.endsWith('s')) return snake;
|
||||
return `${snake}s`;
|
||||
};
|
||||
|
||||
const fromApi = toSnakePlural(apiName);
|
||||
const fromLabel = objectLabel ? toSnakePlural(objectLabel) : null;
|
||||
const fromPlural = pluralLabel ? toSnakePlural(pluralLabel) : null;
|
||||
|
||||
if (fromLabel && fromLabel.includes('_') && !fromApi.includes('_')) {
|
||||
return fromLabel;
|
||||
}
|
||||
if (fromPlural && fromPlural.includes('_') && !fromApi.includes('_')) {
|
||||
return fromPlural;
|
||||
}
|
||||
|
||||
if (fromLabel && fromLabel !== fromApi) return fromLabel;
|
||||
if (fromPlural && fromPlural !== fromApi) return fromPlural;
|
||||
|
||||
return fromApi;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user