diff --git a/backend/src/object/models/dynamic-model.factory.ts b/backend/src/object/models/dynamic-model.factory.ts index ff22f14..3ea871c 100644 --- a/backend/src/object/models/dynamic-model.factory.ts +++ b/backend/src/object/models/dynamic-model.factory.ts @@ -143,15 +143,15 @@ export class DynamicModelFactory { this.id = randomUUID(); } if (!this.created_at) { - this.created_at = new Date().toISOString(); + this.created_at = new Date().toISOString().slice(0, 19).replace('T', ' '); } if (!this.updated_at) { - this.updated_at = new Date().toISOString(); + this.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' '); } } async $beforeUpdate() { - this.updated_at = new Date().toISOString(); + this.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' '); } } diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts index 26aa258..bbbb9e9 100644 --- a/backend/src/object/object.service.ts +++ b/backend/src/object/object.service.ts @@ -468,73 +468,46 @@ export class ObjectService { const tableName = this.getTableName(objectApiName); - // Ensure model is registered before attempting to use it + // Ensure model is registered await this.ensureModelRegistered(resolvedTenantId, objectApiName); - // Try to use the Objection model if available - let records = []; - try { - const Model = this.modelService.getModel(resolvedTenantId, objectApiName); - if (Model) { - const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); - let query = boundModel.query(); - - // Apply authorization scope (modifies query in place) - await this.authService.applyScopeToQuery( - query, - objectDefModel, - user, - 'read', - knex, - ); - - // Build graph expression for lookup fields - const lookupFields = objectDefModel.fields?.filter(f => - f.type === 'LOOKUP' && f.referenceObject - ) || []; - - if (lookupFields.length > 0) { - // Build relation expression - use singular lowercase for relation name - const relationExpression = lookupFields - .map(f => f.apiName.replace(/Id$/, '').toLowerCase()) - .filter(Boolean) - .join(', '); - - if (relationExpression) { - query = query.withGraphFetched(`[${relationExpression}]`); - } - } - - // Apply additional filters - if (filters) { - query = query.where(filters); - } - - records = await query.select('*'); + // Use Objection model + const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); + let query = boundModel.query(); + + // Apply authorization scope (modifies query in place) + await this.authService.applyScopeToQuery( + query, + objectDefModel, + user, + 'read', + knex, + ); + + // Build graph expression for lookup fields + const lookupFields = objectDefModel.fields?.filter(f => + f.type === 'LOOKUP' && f.referenceObject + ) || []; + + if (lookupFields.length > 0) { + // Build relation expression - use singular lowercase for relation name + const relationExpression = lookupFields + .map(f => f.apiName.replace(/Id$/, '').toLowerCase()) + .filter(Boolean) + .join(', '); + + if (relationExpression) { + query = query.withGraphFetched(`[${relationExpression}]`); } - } catch (error) { - this.logger.warn(`Could not use Objection model for ${objectApiName}, falling back to manual query: ${error.message}`); - - // Fallback to Knex query with authorization - let query = knex(tableName); - - // Apply additional filters before authorization scope - if (filters) { - query = query.where(filters); - } - - // Apply authorization scope (modifies query in place) - await this.authService.applyScopeToQuery( - query, - objectDefModel, - user, - 'read', - knex, - ); - - records = await query.select('*'); } + // Apply additional filters + if (filters) { + query = query.where(filters); + } + + const records = await query.select('*'); + // Filter fields based on field-level permissions const filteredRecords = await Promise.all( records.map(record => @@ -554,93 +527,62 @@ export class ObjectService { const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); - // Verify object exists and get field definitions - const objectDef = await this.getObjectDefinition(tenantId, objectApiName); + // Get user with roles and permissions + const user = await User.query(knex) + .findById(userId) + .withGraphFetched('[roles.[objectPermissions, fieldPermissions]]'); - const tableName = this.getTableName(objectApiName); + if (!user) { + throw new NotFoundException('User not found'); + } - // Ensure model is registered before attempting to use it + // Get object definition with authorization settings + const objectDefModel = await ObjectDefinition.query(knex) + .findOne({ apiName: objectApiName }) + .withGraphFetched('fields'); + + if (!objectDefModel) { + throw new NotFoundException(`Object ${objectApiName} not found`); + } + + // Ensure model is registered await this.ensureModelRegistered(resolvedTenantId, objectApiName); - // Try to use the Objection model if available - try { - const Model = this.modelService.getModel(resolvedTenantId, objectApiName); - if (Model) { - const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); - let query = boundModel.query().where({ id: recordId }); - - // Build graph expression for lookup fields - const lookupFields = objectDef.fields?.filter(f => - f.type === 'LOOKUP' && f.referenceObject - ) || []; - - if (lookupFields.length > 0) { - // Build relation expression - use singular lowercase for relation name - const relationExpression = lookupFields - .map(f => f.apiName.replace(/Id$/, '').toLowerCase()) - .filter(Boolean) - .join(', '); - - if (relationExpression) { - query = query.withGraphFetched(`[${relationExpression}]`); - } - } - - // Add ownership filter if ownerId field exists - const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId'); - if (hasOwner) { - query = query.where({ ownerId: userId }); - } - - const record = await query.first(); - if (!record) { - throw new NotFoundException('Record not found'); - } - return record; - } - } catch (error) { - this.logger.warn(`Could not use Objection model for ${objectApiName}, falling back to manual join: ${error.message}`); - } + // Use Objection model + const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); + let query = boundModel.query().where({ id: recordId }); - // Fallback to manual data hydration - let query = knex(tableName).where({ [`${tableName}.id`]: recordId }); + // Apply authorization scope (modifies query in place) + await this.authService.applyScopeToQuery( + query, + objectDefModel, + user, + 'read', + knex, + ); - // Add ownership filter if ownerId field exists - const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId'); - if (hasOwner) { - query = query.where({ [`${tableName}.ownerId`]: userId }); - } - - const record = await query.first(); - - if (!record) { - throw new NotFoundException('Record not found'); - } - - // Fetch and attach related records for lookup fields - const lookupFields = objectDef.fields?.filter(f => + // Build graph expression for lookup fields + const lookupFields = objectDefModel.fields?.filter(f => f.type === 'LOOKUP' && f.referenceObject ) || []; if (lookupFields.length > 0) { - for (const field of lookupFields) { - const relationName = field.apiName.replace(/Id$/, '').toLowerCase(); - const relatedTable = this.getTableName(field.referenceObject); - const relatedId = record[field.apiName]; - - if (relatedId) { - // Fetch the related record - const relatedRecord = await knex(relatedTable) - .where({ id: relatedId }) - .first(); - - if (relatedRecord) { - record[relationName] = relatedRecord; - } - } + // Build relation expression - use singular lowercase for relation name + const relationExpression = lookupFields + .map(f => f.apiName.replace(/Id$/, '').toLowerCase()) + .filter(Boolean) + .join(', '); + + if (relationExpression) { + query = query.withGraphFetched(`[${relationExpression}]`); } } + const record = await query.first(); + if (!record) { + throw new NotFoundException('Record not found'); + } + return record; } @@ -680,47 +622,17 @@ export class ObjectService { // Filter data to only editable fields const editableData = await this.authService.filterEditableFields(data, objectDefModel.fields, user); - // Ensure model is registered before attempting to use it + // Ensure model is registered await this.ensureModelRegistered(resolvedTenantId, objectApiName); - // Try to use the Objection model if available - try { - const Model = this.modelService.getModel(resolvedTenantId, objectApiName); - if (Model) { - const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); - const recordData = { - ...editableData, - ownerId: userId, // Auto-set owner - }; - const record = await boundModel.query().insert(recordData); - return record; - } - } catch (error) { - console.warn(`Could not use Objection model for ${objectApiName}:`, error.message); - } - - // Fallback to raw Knex if model not available - const tableName = this.getTableName(objectApiName); - const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId'); - - // Generate UUID for the record - const result = await knex.raw('SELECT UUID() as uuid'); - const uuid = result[0][0].uuid; - - const recordData: any = { - id: uuid, + // Use Objection model + const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); + const recordData = { ...editableData, - created_at: knex.fn.now(), - updated_at: knex.fn.now(), + ownerId: userId, // Auto-set owner }; - - if (hasOwner) { - recordData.ownerId = userId; - } - - await knex(tableName).insert(recordData); - - return knex(tableName).where({ id: uuid }).first(); + const record = await boundModel.query().insert(recordData); + return record; } async updateRecord( @@ -771,27 +683,13 @@ export class ObjectService { delete editableData.created_at; delete editableData.tenantId; - // Ensure model is registered before attempting to use it + // Ensure model is registered await this.ensureModelRegistered(resolvedTenantId, objectApiName); - // Try to use the Objection model if available - try { - const Model = this.modelService.getModel(resolvedTenantId, objectApiName); - if (Model) { - const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); - await boundModel.query().where({ id: recordId }).update(editableData); - return boundModel.query().where({ id: recordId }).first(); - } - } catch (error) { - console.warn(`Could not use Objection model for ${objectApiName}:`, error.message); - } - - // Fallback to raw Knex - await knex(tableName) - .where({ id: recordId }) - .update({ ...editableData, updated_at: knex.fn.now() }); - - return knex(tableName).where({ id: recordId }).first(); + // Use Objection model + const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); + await boundModel.query().where({ id: recordId }).update(editableData); + return boundModel.query().where({ id: recordId }).first(); } async deleteRecord( @@ -831,23 +729,12 @@ export class ObjectService { // Check if user can delete this record await this.authService.assertCanPerformAction('delete', objectDefModel, existingRecord, user, knex); - // Ensure model is registered before attempting to use it + // Ensure model is registered await this.ensureModelRegistered(resolvedTenantId, objectApiName); - // Try to use the Objection model if available - try { - const Model = this.modelService.getModel(resolvedTenantId, objectApiName); - if (Model) { - const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); - await boundModel.query().where({ id: recordId }).delete(); - return { success: true }; - } - } catch (error) { - console.warn(`Could not use Objection model for ${objectApiName}:`, error.message); - } - - // Fallback to raw Knex - await knex(tableName).where({ id: recordId }).delete(); + // Use Objection model + const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); + await boundModel.query().where({ id: recordId }).delete(); return { success: true }; } diff --git a/backend/src/rbac/authorization.service.ts b/backend/src/rbac/authorization.service.ts index 31a53a8..45d2efe 100644 --- a/backend/src/rbac/authorization.service.ts +++ b/backend/src/rbac/authorization.service.ts @@ -120,7 +120,13 @@ export class AuthorizationService { const hasViewAll = ability.can('view_all', objectDef.id); const hasModifyAll = ability.can('modify_all', objectDef.id); - if (hasViewAll || hasModifyAll) { + // canViewAll only grants read access to all records + if (action === 'read' && hasViewAll) { + return true; + } + + // canModifyAll grants edit/delete access to all records + if ((action === 'update' || action === 'delete') && hasModifyAll) { return true; }