WIP - better handling of viewAll modifyAll

This commit is contained in:
Francisco Gaona
2025-12-30 04:43:51 +01:00
parent d37183ba45
commit 9ac69e30d0
3 changed files with 104 additions and 211 deletions

View File

@@ -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', ' ');
}
}

View File

@@ -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 };
}

View File

@@ -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;
}