WIP - better handling of viewAll modifyAll
This commit is contained in:
@@ -143,15 +143,15 @@ export class DynamicModelFactory {
|
|||||||
this.id = randomUUID();
|
this.id = randomUUID();
|
||||||
}
|
}
|
||||||
if (!this.created_at) {
|
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) {
|
if (!this.updated_at) {
|
||||||
this.updated_at = new Date().toISOString();
|
this.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate() {
|
async $beforeUpdate() {
|
||||||
this.updated_at = new Date().toISOString();
|
this.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -468,73 +468,46 @@ export class ObjectService {
|
|||||||
|
|
||||||
const tableName = this.getTableName(objectApiName);
|
const tableName = this.getTableName(objectApiName);
|
||||||
|
|
||||||
// Ensure model is registered before attempting to use it
|
// Ensure model is registered
|
||||||
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
||||||
|
|
||||||
// Try to use the Objection model if available
|
// Use Objection model
|
||||||
let records = [];
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
||||||
try {
|
let query = boundModel.query();
|
||||||
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)
|
// Apply authorization scope (modifies query in place)
|
||||||
await this.authService.applyScopeToQuery(
|
await this.authService.applyScopeToQuery(
|
||||||
query,
|
query,
|
||||||
objectDefModel,
|
objectDefModel,
|
||||||
user,
|
user,
|
||||||
'read',
|
'read',
|
||||||
knex,
|
knex,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build graph expression for lookup fields
|
// Build graph expression for lookup fields
|
||||||
const lookupFields = objectDefModel.fields?.filter(f =>
|
const lookupFields = objectDefModel.fields?.filter(f =>
|
||||||
f.type === 'LOOKUP' && f.referenceObject
|
f.type === 'LOOKUP' && f.referenceObject
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
if (lookupFields.length > 0) {
|
if (lookupFields.length > 0) {
|
||||||
// Build relation expression - use singular lowercase for relation name
|
// Build relation expression - use singular lowercase for relation name
|
||||||
const relationExpression = lookupFields
|
const relationExpression = lookupFields
|
||||||
.map(f => f.apiName.replace(/Id$/, '').toLowerCase())
|
.map(f => f.apiName.replace(/Id$/, '').toLowerCase())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
if (relationExpression) {
|
if (relationExpression) {
|
||||||
query = query.withGraphFetched(`[${relationExpression}]`);
|
query = query.withGraphFetched(`[${relationExpression}]`);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply additional filters
|
|
||||||
if (filters) {
|
|
||||||
query = query.where(filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
records = await query.select('*');
|
|
||||||
}
|
}
|
||||||
} 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
|
// Filter fields based on field-level permissions
|
||||||
const filteredRecords = await Promise.all(
|
const filteredRecords = await Promise.all(
|
||||||
records.map(record =>
|
records.map(record =>
|
||||||
@@ -554,93 +527,62 @@ export class ObjectService {
|
|||||||
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
// Verify object exists and get field definitions
|
// Get user with roles and permissions
|
||||||
const objectDef = await this.getObjectDefinition(tenantId, objectApiName);
|
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);
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
||||||
|
|
||||||
// Try to use the Objection model if available
|
// Use Objection model
|
||||||
try {
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
||||||
const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
|
let query = boundModel.query().where({ id: recordId });
|
||||||
if (Model) {
|
|
||||||
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
|
||||||
let query = boundModel.query().where({ id: recordId });
|
|
||||||
|
|
||||||
// Build graph expression for lookup fields
|
// Apply authorization scope (modifies query in place)
|
||||||
const lookupFields = objectDef.fields?.filter(f =>
|
await this.authService.applyScopeToQuery(
|
||||||
f.type === 'LOOKUP' && f.referenceObject
|
query,
|
||||||
) || [];
|
objectDefModel,
|
||||||
|
user,
|
||||||
|
'read',
|
||||||
|
knex,
|
||||||
|
);
|
||||||
|
|
||||||
if (lookupFields.length > 0) {
|
// Build graph expression for lookup fields
|
||||||
// Build relation expression - use singular lowercase for relation name
|
const lookupFields = objectDefModel.fields?.filter(f =>
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to manual data hydration
|
|
||||||
let query = knex(tableName).where({ [`${tableName}.id`]: recordId });
|
|
||||||
|
|
||||||
// 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 =>
|
|
||||||
f.type === 'LOOKUP' && f.referenceObject
|
f.type === 'LOOKUP' && f.referenceObject
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
if (lookupFields.length > 0) {
|
if (lookupFields.length > 0) {
|
||||||
for (const field of lookupFields) {
|
// Build relation expression - use singular lowercase for relation name
|
||||||
const relationName = field.apiName.replace(/Id$/, '').toLowerCase();
|
const relationExpression = lookupFields
|
||||||
const relatedTable = this.getTableName(field.referenceObject);
|
.map(f => f.apiName.replace(/Id$/, '').toLowerCase())
|
||||||
const relatedId = record[field.apiName];
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
if (relatedId) {
|
if (relationExpression) {
|
||||||
// Fetch the related record
|
query = query.withGraphFetched(`[${relationExpression}]`);
|
||||||
const relatedRecord = await knex(relatedTable)
|
|
||||||
.where({ id: relatedId })
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (relatedRecord) {
|
|
||||||
record[relationName] = relatedRecord;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const record = await query.first();
|
||||||
|
if (!record) {
|
||||||
|
throw new NotFoundException('Record not found');
|
||||||
|
}
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,47 +622,17 @@ export class ObjectService {
|
|||||||
// Filter data to only editable fields
|
// Filter data to only editable fields
|
||||||
const editableData = await this.authService.filterEditableFields(data, objectDefModel.fields, user);
|
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);
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
||||||
|
|
||||||
// Try to use the Objection model if available
|
// Use Objection model
|
||||||
try {
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
||||||
const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
|
const recordData = {
|
||||||
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,
|
|
||||||
...editableData,
|
...editableData,
|
||||||
created_at: knex.fn.now(),
|
ownerId: userId, // Auto-set owner
|
||||||
updated_at: knex.fn.now(),
|
|
||||||
};
|
};
|
||||||
|
const record = await boundModel.query().insert(recordData);
|
||||||
if (hasOwner) {
|
return record;
|
||||||
recordData.ownerId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await knex(tableName).insert(recordData);
|
|
||||||
|
|
||||||
return knex(tableName).where({ id: uuid }).first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateRecord(
|
async updateRecord(
|
||||||
@@ -771,27 +683,13 @@ export class ObjectService {
|
|||||||
delete editableData.created_at;
|
delete editableData.created_at;
|
||||||
delete editableData.tenantId;
|
delete editableData.tenantId;
|
||||||
|
|
||||||
// Ensure model is registered before attempting to use it
|
// Ensure model is registered
|
||||||
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
||||||
|
|
||||||
// Try to use the Objection model if available
|
// Use Objection model
|
||||||
try {
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
||||||
const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
|
await boundModel.query().where({ id: recordId }).update(editableData);
|
||||||
if (Model) {
|
return boundModel.query().where({ id: recordId }).first();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteRecord(
|
async deleteRecord(
|
||||||
@@ -831,23 +729,12 @@ export class ObjectService {
|
|||||||
// Check if user can delete this record
|
// Check if user can delete this record
|
||||||
await this.authService.assertCanPerformAction('delete', objectDefModel, existingRecord, user, knex);
|
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);
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
||||||
|
|
||||||
// Try to use the Objection model if available
|
// Use Objection model
|
||||||
try {
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
||||||
const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
|
await boundModel.query().where({ id: recordId }).delete();
|
||||||
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();
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,13 @@ export class AuthorizationService {
|
|||||||
const hasViewAll = ability.can('view_all', objectDef.id);
|
const hasViewAll = ability.can('view_all', objectDef.id);
|
||||||
const hasModifyAll = ability.can('modify_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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user