WIP - add contct and contact details

This commit is contained in:
Francisco Gaona
2026-01-12 21:08:47 +01:00
parent f8a3cffb64
commit ca11c8cbe7
19 changed files with 551 additions and 94 deletions

View File

@@ -107,18 +107,23 @@ exports.up = async function (knex) {
(await knex('object_definitions').where('apiName', 'ContactDetail').first())
.id;
const contactDetailRelationObjects = ['Account', 'Contact']
await knex('field_definitions').insert([
{
id: knex.raw('(UUID())'),
objectDefinitionId: contactDetailObjectDefId,
apiName: 'relatedObjectType',
label: 'Related Object Type',
type: 'String',
type: 'PICKLIST',
length: 100,
isRequired: true,
isSystem: true,
isSystem: false,
isCustom: false,
displayOrder: 1,
ui_metadata: JSON.stringify({
options: contactDetailRelationObjects.map((value) => ({ label: value, value })),
}),
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
},
@@ -127,12 +132,17 @@ exports.up = async function (knex) {
objectDefinitionId: contactDetailObjectDefId,
apiName: 'relatedObjectId',
label: 'Related Object ID',
type: 'String',
type: 'LOOKUP',
length: 36,
isRequired: true,
isSystem: true,
isSystem: false,
isCustom: false,
displayOrder: 2,
ui_metadata: JSON.stringify({
relationObjects: contactDetailRelationObjects,
relationTypeField: 'relatedObjectType',
relationDisplayField: 'name',
}),
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
},
@@ -144,7 +154,7 @@ exports.up = async function (knex) {
type: 'String',
length: 50,
isRequired: true,
isSystem: true,
isSystem: false,
isCustom: false,
displayOrder: 3,
created_at: knex.fn.now(),
@@ -157,7 +167,7 @@ exports.up = async function (knex) {
label: 'Label',
type: 'String',
length: 100,
isSystem: true,
isSystem: false,
isCustom: false,
displayOrder: 4,
created_at: knex.fn.now(),
@@ -170,7 +180,7 @@ exports.up = async function (knex) {
label: 'Value',
type: 'Text',
isRequired: true,
isSystem: true,
isSystem: false,
isCustom: false,
displayOrder: 5,
created_at: knex.fn.now(),
@@ -182,7 +192,7 @@ exports.up = async function (knex) {
apiName: 'isPrimary',
label: 'Primary',
type: 'Boolean',
isSystem: true,
isSystem: false,
isCustom: false,
displayOrder: 6,
created_at: knex.fn.now(),

View File

@@ -0,0 +1,101 @@
exports.up = async function (knex) {
const contactDetailObject = await knex('object_definitions')
.where({ apiName: 'ContactDetail' })
.first();
if (!contactDetailObject) return;
const relationObjects = ['Account', 'Contact'];
await knex('field_definitions')
.where({
objectDefinitionId: contactDetailObject.id,
apiName: 'relatedObjectType',
})
.update({
type: 'PICKLIST',
length: 100,
isSystem: false,
ui_metadata: JSON.stringify({
options: relationObjects.map((value) => ({ label: value, value })),
}),
updated_at: knex.fn.now(),
});
await knex('field_definitions')
.where({
objectDefinitionId: contactDetailObject.id,
apiName: 'relatedObjectId',
})
.update({
type: 'LOOKUP',
length: 36,
isSystem: false,
ui_metadata: JSON.stringify({
relationObjects,
relationTypeField: 'relatedObjectType',
relationDisplayField: 'name',
}),
updated_at: knex.fn.now(),
});
await knex('field_definitions')
.whereIn('apiName', [
'detailType',
'label',
'value',
'isPrimary',
])
.andWhere({ objectDefinitionId: contactDetailObject.id })
.update({
isSystem: false,
updated_at: knex.fn.now(),
});
};
exports.down = async function (knex) {
const contactDetailObject = await knex('object_definitions')
.where({ apiName: 'ContactDetail' })
.first();
if (!contactDetailObject) return;
await knex('field_definitions')
.where({
objectDefinitionId: contactDetailObject.id,
apiName: 'relatedObjectType',
})
.update({
type: 'String',
length: 100,
isSystem: true,
ui_metadata: null,
updated_at: knex.fn.now(),
});
await knex('field_definitions')
.where({
objectDefinitionId: contactDetailObject.id,
apiName: 'relatedObjectId',
})
.update({
type: 'String',
length: 36,
isSystem: true,
ui_metadata: null,
updated_at: knex.fn.now(),
});
await knex('field_definitions')
.whereIn('apiName', [
'detailType',
'label',
'value',
'isPrimary',
])
.andWhere({ objectDefinitionId: contactDetailObject.id })
.update({
isSystem: true,
updated_at: knex.fn.now(),
});
};

View File

@@ -0,0 +1,45 @@
exports.up = async function (knex) {
const contactDetailObject = await knex('object_definitions')
.where({ apiName: 'ContactDetail' })
.first();
if (!contactDetailObject) return;
await knex('field_definitions')
.where({ objectDefinitionId: contactDetailObject.id })
.whereIn('apiName', [
'relatedObjectType',
'relatedObjectId',
'detailType',
'label',
'value',
'isPrimary',
])
.update({
isSystem: false,
updated_at: knex.fn.now(),
});
};
exports.down = async function (knex) {
const contactDetailObject = await knex('object_definitions')
.where({ apiName: 'ContactDetail' })
.first();
if (!contactDetailObject) return;
await knex('field_definitions')
.where({ objectDefinitionId: contactDetailObject.id })
.whereIn('apiName', [
'relatedObjectType',
'relatedObjectId',
'detailType',
'label',
'value',
'isPrimary',
])
.update({
isSystem: true,
updated_at: knex.fn.now(),
});
};

View File

@@ -4,10 +4,30 @@ export class ContactDetail extends BaseModel {
static tableName = 'contact_details';
id!: string;
relatedObjectType!: string;
relatedObjectType!: 'Account' | 'Contact';
relatedObjectId!: string;
detailType!: string;
label?: string;
value!: string;
isPrimary!: boolean;
// Provide optional relations for each supported parent type.
static relationMappings = {
account: {
relation: BaseModel.BelongsToOneRelation,
modelClass: 'account.model',
join: {
from: 'contact_details.relatedObjectId',
to: 'accounts.id',
},
},
contact: {
relation: BaseModel.BelongsToOneRelation,
modelClass: 'contact.model',
join: {
from: 'contact_details.relatedObjectId',
to: 'contacts.id',
},
},
};
}

View File

@@ -30,6 +30,8 @@ export interface UIMetadata {
step?: number; // For number
accept?: string; // For file/image
relationDisplayField?: string; // Which field to display for relations
relationObjects?: string[]; // For polymorphic relations
relationTypeField?: string; // Field API name storing the selected relation type
// Formatting
format?: string; // Date format, number format, etc.

View File

@@ -22,7 +22,9 @@ export interface FieldConfigDTO {
step?: number;
accept?: string;
relationObject?: string;
relationObjects?: string[];
relationDisplayField?: string;
relationTypeField?: string;
format?: string;
prefix?: string;
suffix?: string;
@@ -106,10 +108,12 @@ export class FieldMapperService {
step: uiMetadata.step,
accept: uiMetadata.accept,
relationObject: field.referenceObject,
relationObjects: uiMetadata.relationObjects,
// For lookup fields, provide default display field if not specified
relationDisplayField: isLookupField
? (uiMetadata.relationDisplayField || 'name')
: uiMetadata.relationDisplayField,
relationTypeField: uiMetadata.relationTypeField,
// Formatting
format: uiMetadata.format,

View File

@@ -16,13 +16,17 @@ export class ModelRegistry {
*/
registerModel(apiName: string, modelClass: ModelClass<BaseModel>): void {
this.registry.set(apiName, modelClass);
const lowerKey = apiName.toLowerCase();
if (lowerKey !== apiName && !this.registry.has(lowerKey)) {
this.registry.set(lowerKey, modelClass);
}
}
/**
* Get a model from the registry
*/
getModel(apiName: string): ModelClass<BaseModel> {
const model = this.registry.get(apiName);
const model = this.registry.get(apiName) || this.registry.get(apiName.toLowerCase());
if (!model) {
throw new Error(`Model for ${apiName} not found in registry`);
}
@@ -33,7 +37,7 @@ export class ModelRegistry {
* Check if a model exists in the registry
*/
hasModel(apiName: string): boolean {
return this.registry.has(apiName);
return this.registry.has(apiName) || this.registry.has(apiName.toLowerCase());
}
/**
@@ -46,7 +50,8 @@ export class ModelRegistry {
// Returns undefined if model not found (for models not yet registered)
const model = DynamicModelFactory.createModel(
metadata,
(apiName: string) => this.registry.get(apiName),
(apiName: string) =>
this.registry.get(apiName) || this.registry.get(apiName.toLowerCase()),
);
this.registerModel(metadata.apiName, model);
return model;

View File

@@ -187,7 +187,7 @@ export class ObjectService {
}
// Create a migration to create the table
const tableName = this.getTableName(data.apiName);
const tableName = this.getTableName(data.apiName, data.label, data.pluralLabel);
const createTableSQL = this.customMigrationService.generateCreateTableSQL(tableName);
try {
@@ -273,6 +273,8 @@ export class ObjectService {
referenceObject?: string;
relationObject?: string;
relationDisplayField?: string;
relationObjects?: string[];
relationTypeField?: string;
defaultValue?: string;
length?: number;
precision?: number;
@@ -311,16 +313,13 @@ export class ObjectService {
updated_at: knex.fn.now(),
};
// Merge UI metadata
const uiMetadata: any = {};
if (data.relationDisplayField) {
uiMetadata.relationDisplayField = data.relationDisplayField;
}
if (data.uiMetadata) {
Object.assign(uiMetadata, data.uiMetadata);
}
if (Object.keys(uiMetadata).length > 0) {
fieldData.ui_metadata = JSON.stringify(uiMetadata);
// Store relationDisplayField in UI metadata if provided
if (data.relationDisplayField || data.relationObjects || data.relationTypeField) {
fieldData.ui_metadata = JSON.stringify({
relationDisplayField: data.relationDisplayField,
relationObjects: data.relationObjects,
relationTypeField: data.relationTypeField,
});
}
await knex('field_definitions').insert(fieldData);
@@ -329,7 +328,13 @@ export class ObjectService {
// Add the column to the physical table
const schemaManagementService = new SchemaManagementService();
try {
await schemaManagementService.addFieldToTable(knex, objectApiName, createdField);
await schemaManagementService.addFieldToTable(
knex,
obj.apiName,
createdField,
obj.label,
obj.pluralLabel,
);
this.logger.log(`Added column ${data.apiName} to table for object ${objectApiName}`);
} catch (error) {
// If column creation fails, delete the field definition to maintain consistency
@@ -342,22 +347,37 @@ export class ObjectService {
}
// Helper to get table name from object definition
private getTableName(objectApiName: string): string {
// Convert CamelCase to snake_case and pluralize
// Account -> accounts, ContactPerson -> contact_persons
const snakeCase = objectApiName
.replace(/([A-Z])/g, '_$1')
.toLowerCase()
.replace(/^_/, '');
// Simple pluralization (can be enhanced)
if (snakeCase.endsWith('y')) {
return snakeCase.slice(0, -1) + 'ies';
} else if (snakeCase.endsWith('s')) {
return snakeCase;
} else {
return snakeCase + 's';
private getTableName(objectApiName: 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(/^_/, '');
if (snake.endsWith('y')) return `${snake.slice(0, -1)}ies`;
if (snake.endsWith('s')) return snake;
return `${snake}s`;
};
const fromApi = toSnakePlural(objectApiName);
const fromLabel = objectLabel ? toSnakePlural(objectLabel) : null;
const fromPlural = pluralLabel ? toSnakePlural(pluralLabel) : null;
// Prefer the label-derived name when it introduces clearer word boundaries
if (fromLabel && fromLabel.includes('_') && !fromApi.includes('_')) {
return fromLabel;
}
if (fromPlural && fromPlural.includes('_') && !fromApi.includes('_')) {
return fromPlural;
}
// Otherwise fall back to label/plural if they differ from API-derived
if (fromLabel && fromLabel !== fromApi) return fromLabel;
if (fromPlural && fromPlural !== fromApi) return fromPlural;
return fromApi;
}
/**
@@ -417,11 +437,16 @@ export class ObjectService {
private async ensureModelRegistered(
tenantId: string,
objectApiName: string,
objectDefinition?: any,
): Promise<void> {
// Provide a metadata fetcher function that the ModelService can use
const fetchMetadata = async (apiName: string): Promise<ObjectMetadata> => {
const objectDef = await this.getObjectDefinition(tenantId, apiName);
const tableName = this.getTableName(apiName);
const tableName = this.getTableName(
objectDef.apiName,
objectDef.label,
objectDef.pluralLabel,
);
// Build relations from lookup fields, but only for models that exist
const lookupFields = objectDef.fields.filter((f: any) =>
@@ -472,7 +497,7 @@ export class ObjectService {
try {
await this.modelService.ensureModelWithDependencies(
tenantId,
objectApiName,
objectDefinition?.apiName || objectApiName,
fetchMetadata,
);
} catch (error) {
@@ -510,10 +535,14 @@ export class ObjectService {
throw new NotFoundException(`Object ${objectApiName} not found`);
}
const tableName = this.getTableName(objectApiName);
const tableName = this.getTableName(
objectDefModel.apiName,
objectDefModel.label,
objectDefModel.pluralLabel,
);
// Ensure model is registered
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
await this.ensureModelRegistered(resolvedTenantId, objectApiName, objectDefModel);
// Use Objection model
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
@@ -590,7 +619,7 @@ export class ObjectService {
}
// Ensure model is registered
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
await this.ensureModelRegistered(resolvedTenantId, objectApiName, objectDefModel);
// Use Objection model
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
@@ -648,19 +677,50 @@ export class ObjectService {
}>> {
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const relatedLookups = await knex('field_definitions as fd')
const relatedLookupsRaw = await knex('field_definitions as fd')
.join('object_definitions as od', 'fd.objectDefinitionId', 'od.id')
.where('fd.type', 'LOOKUP')
.andWhere('fd.referenceObject', objectApiName)
.select(
'fd.apiName as fieldApiName',
'fd.label as fieldLabel',
'fd.objectDefinitionId as objectDefinitionId',
'fd.referenceObject as referenceObject',
'fd.ui_metadata as uiMetadata',
'od.apiName as childApiName',
'od.label as childLabel',
'od.pluralLabel as childPluralLabel',
);
const relatedLookups = relatedLookupsRaw
.map((lookup: any) => {
let uiMetadata: any = {};
if (lookup.uiMetadata) {
try {
uiMetadata = typeof lookup.uiMetadata === 'string'
? JSON.parse(lookup.uiMetadata)
: lookup.uiMetadata;
} catch {
uiMetadata = {};
}
}
return { ...lookup, uiMetadata };
})
.filter((lookup: any) => {
const target = (objectApiName || '').toLowerCase();
const referenceMatch =
typeof lookup.referenceObject === 'string' &&
lookup.referenceObject.toLowerCase() === target;
if (referenceMatch) return true;
const relationObjects = lookup.uiMetadata?.relationObjects;
if (!Array.isArray(relationObjects)) return false;
return relationObjects.some(
(rel: string) => typeof rel === 'string' && rel.toLowerCase() === target,
);
});
if (relatedLookups.length === 0) {
return [];
}
@@ -689,7 +749,11 @@ export class ObjectService {
);
return relatedLookups.map((lookup: any) => {
const baseRelationName = this.getTableName(lookup.childApiName);
const baseRelationName = this.getTableName(
lookup.childApiName,
lookup.childLabel,
lookup.childPluralLabel,
);
const hasMultiple = lookupCounts[lookup.childApiName] > 1;
const relationName = hasMultiple
? `${baseRelationName}_${lookup.fieldApiName.replace(/Id$/, '').toLowerCase()}`
@@ -747,13 +811,14 @@ export class ObjectService {
const editableData = await this.authService.filterEditableFields(data, objectDefModel.fields, user);
// Ensure model is registered
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
await this.ensureModelRegistered(resolvedTenantId, objectApiName, objectDefModel);
// Use Objection model
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
const hasOwnerField = objectDefModel.fields?.some((f: any) => f.apiName === 'ownerId');
const recordData = {
...editableData,
ownerId: userId, // Auto-set owner
...(hasOwnerField ? { ownerId: userId } : {}),
};
const record = await boundModel.query().insert(recordData);
return record;
@@ -787,7 +852,11 @@ export class ObjectService {
throw new NotFoundException(`Object ${objectApiName} not found`);
}
const tableName = this.getTableName(objectApiName);
const tableName = this.getTableName(
objectDefModel.apiName,
objectDefModel.label,
objectDefModel.pluralLabel,
);
// Get existing record
const existingRecord = await knex(tableName).where({ id: recordId }).first();
@@ -808,7 +877,7 @@ export class ObjectService {
delete editableData.tenantId;
// Ensure model is registered
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
await this.ensureModelRegistered(resolvedTenantId, objectApiName, objectDefModel);
// Use Objection model
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
@@ -842,7 +911,11 @@ export class ObjectService {
throw new NotFoundException(`Object ${objectApiName} not found`);
}
const tableName = this.getTableName(objectApiName);
const tableName = this.getTableName(
objectDefModel.apiName,
objectDefModel.label,
objectDefModel.pluralLabel,
);
// Get existing record
const existingRecord = await knex(tableName).where({ id: recordId }).first();
@@ -854,7 +927,7 @@ export class ObjectService {
await this.authService.assertCanPerformAction('delete', objectDefModel, existingRecord, user, knex);
// Ensure model is registered
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
await this.ensureModelRegistered(resolvedTenantId, objectApiName, objectDefModel);
// Use Objection model
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
@@ -905,7 +978,11 @@ export class ObjectService {
}
// Check if this field has data (count records)
const tableName = this.getTableName(objectApiName);
const tableName = this.getTableName(
objectDef.apiName,
objectDef.label,
objectDef.pluralLabel,
);
const recordCount = await knex(tableName).count('* as cnt').first();
const hasData = recordCount && (recordCount.cnt as number) > 0;
@@ -1060,11 +1137,21 @@ export class ObjectService {
}
// Remove the column from the physical table
const tableName = this.getTableName(objectApiName);
const tableName = this.getTableName(
objectDef.apiName,
objectDef.label,
objectDef.pluralLabel,
);
const schemaManagementService = new SchemaManagementService();
try {
await schemaManagementService.removeFieldFromTable(knex, objectApiName, fieldApiName);
await schemaManagementService.removeFieldFromTable(
knex,
objectDef.apiName,
fieldApiName,
objectDef.label,
objectDef.pluralLabel,
);
} catch (error) {
this.logger.warn(`Failed to remove column ${fieldApiName} from table ${tableName}: ${error.message}`);
// Continue with deletion even if column removal fails - field definition must be cleaned up

View File

@@ -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);
@@ -81,11 +89,13 @@ export class SchemaManagementService {
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);
const tableName = this.getTableName(objectApiName, objectLabel, pluralLabel);
const skipTypeChange = options?.skipTypeChange ?? true;
await knex.schema.alterTable(tableName, (table) => {
@@ -105,8 +115,8 @@ export class SchemaManagementService {
/**
* 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);
@@ -241,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;
}
/**

View File

@@ -180,8 +180,9 @@ export class AbilityFactory {
}
}
// Field permissions exist but this field is not explicitly granted → deny
return false;
// No explicit rule for this field but other field permissions exist.
// Default to allow so new fields don't get silently stripped and fail validation.
return true;
}
/**

View File

@@ -45,7 +45,11 @@ export class RecordSharingController {
}
// Get the record to check ownership
const tableName = this.getTableName(objectDef.apiName);
const tableName = this.getTableName(
objectDef.apiName,
objectDef.label,
objectDef.pluralLabel,
);
const record = await knex(tableName)
.where({ id: recordId })
.first();
@@ -109,7 +113,11 @@ export class RecordSharingController {
}
// Get the record to check ownership
const tableName = this.getTableName(objectDef.apiName);
const tableName = this.getTableName(
objectDef.apiName,
objectDef.label,
objectDef.pluralLabel,
);
const record = await knex(tableName)
.where({ id: recordId })
.first();
@@ -207,7 +215,11 @@ export class RecordSharingController {
}
// Get the record to check ownership
const tableName = this.getTableName(objectDef.apiName);
const tableName = this.getTableName(
objectDef.apiName,
objectDef.label,
objectDef.pluralLabel,
);
const record = await knex(tableName)
.where({ id: recordId })
.first();
@@ -305,20 +317,34 @@ export class RecordSharingController {
return false;
}
private getTableName(apiName: string): string {
// Convert CamelCase to snake_case and pluralize
const snakeCase = apiName
.replace(/([A-Z])/g, '_$1')
.toLowerCase()
.replace(/^_/, '');
// Simple pluralization
if (snakeCase.endsWith('y')) {
return snakeCase.slice(0, -1) + 'ies';
} else if (snakeCase.endsWith('s')) {
return snakeCase + 'es';
} else {
return snakeCase + 's';
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(/^_/, '');
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;
}
}