|
|
|
|
@@ -2,6 +2,10 @@ import { Injectable, NotFoundException, Logger } from '@nestjs/common';
|
|
|
|
|
import { TenantDatabaseService } from '../tenant/tenant-database.service';
|
|
|
|
|
import { CustomMigrationService } from '../migration/custom-migration.service';
|
|
|
|
|
import { ModelService } from './models/model.service';
|
|
|
|
|
import { AuthorizationService } from '../rbac/authorization.service';
|
|
|
|
|
import { ObjectDefinition } from '../models/object-definition.model';
|
|
|
|
|
import { FieldDefinition } from '../models/field-definition.model';
|
|
|
|
|
import { User } from '../models/user.model';
|
|
|
|
|
import { ObjectMetadata } from './models/dynamic-model.factory';
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
@@ -12,6 +16,7 @@ export class ObjectService {
|
|
|
|
|
private tenantDbService: TenantDatabaseService,
|
|
|
|
|
private customMigrationService: CustomMigrationService,
|
|
|
|
|
private modelService: ModelService,
|
|
|
|
|
private authService: AuthorizationService,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
// Setup endpoints - Object metadata management
|
|
|
|
|
@@ -225,6 +230,31 @@ export class ObjectService {
|
|
|
|
|
return objectDef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateObjectDefinition(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
objectApiName: string,
|
|
|
|
|
data: Partial<{
|
|
|
|
|
label: string;
|
|
|
|
|
pluralLabel: string;
|
|
|
|
|
description: string;
|
|
|
|
|
orgWideDefault: 'private' | 'public_read' | 'public_read_write';
|
|
|
|
|
}>,
|
|
|
|
|
) {
|
|
|
|
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
|
|
|
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
|
|
|
|
|
|
|
|
|
// Update the object definition
|
|
|
|
|
await ObjectDefinition.query(knex)
|
|
|
|
|
.findOne({ apiName: objectApiName })
|
|
|
|
|
.patch({
|
|
|
|
|
...data,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Return updated object
|
|
|
|
|
return await ObjectDefinition.query(knex)
|
|
|
|
|
.findOne({ apiName: objectApiName });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createFieldDefinition(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
@@ -418,8 +448,23 @@ 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]]');
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tableName = this.getTableName(objectApiName);
|
|
|
|
|
|
|
|
|
|
@@ -427,14 +472,24 @@ export class ObjectService {
|
|
|
|
|
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 = objectDef.fields?.filter(f =>
|
|
|
|
|
const lookupFields = objectDefModel.fields?.filter(f =>
|
|
|
|
|
f.type === 'LOOKUP' && f.referenceObject
|
|
|
|
|
) || [];
|
|
|
|
|
|
|
|
|
|
@@ -450,80 +505,44 @@ export class ObjectService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add ownership filter if ownerId field exists
|
|
|
|
|
const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId');
|
|
|
|
|
if (hasOwner) {
|
|
|
|
|
query = query.where({ ownerId: userId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply additional filters
|
|
|
|
|
if (filters) {
|
|
|
|
|
query = query.where(filters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return query.select('*');
|
|
|
|
|
records = await query.select('*');
|
|
|
|
|
}
|
|
|
|
|
} 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);
|
|
|
|
|
|
|
|
|
|
// Add ownership filter if ownerId field exists
|
|
|
|
|
const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId');
|
|
|
|
|
if (hasOwner) {
|
|
|
|
|
query = query.where({ [`${tableName}.ownerId`]: userId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply additional filters
|
|
|
|
|
if (filters) {
|
|
|
|
|
query = query.where(filters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get base records
|
|
|
|
|
const records = await query.select(`${tableName}.*`);
|
|
|
|
|
|
|
|
|
|
// Fetch and attach related records for lookup fields
|
|
|
|
|
const lookupFields = objectDef.fields?.filter(f =>
|
|
|
|
|
f.type === 'LOOKUP' && f.referenceObject
|
|
|
|
|
) || [];
|
|
|
|
|
|
|
|
|
|
if (lookupFields.length > 0 && records.length > 0) {
|
|
|
|
|
for (const field of lookupFields) {
|
|
|
|
|
const relationName = field.apiName.replace(/Id$/, '').toLowerCase();
|
|
|
|
|
const relatedTable = this.getTableName(field.referenceObject);
|
|
|
|
|
|
|
|
|
|
// Get unique IDs to fetch
|
|
|
|
|
const relatedIds = [...new Set(
|
|
|
|
|
records
|
|
|
|
|
.map(r => r[field.apiName])
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
)];
|
|
|
|
|
|
|
|
|
|
if (relatedIds.length > 0) {
|
|
|
|
|
// Fetch all related records in one query
|
|
|
|
|
const relatedRecords = await knex(relatedTable)
|
|
|
|
|
.whereIn('id', relatedIds)
|
|
|
|
|
.select('*');
|
|
|
|
|
|
|
|
|
|
// Create a map for quick lookup
|
|
|
|
|
const relatedMap = new Map(
|
|
|
|
|
relatedRecords.map(r => [r.id, r])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Attach related records to main records
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
const relatedId = record[field.apiName];
|
|
|
|
|
if (relatedId && relatedMap.has(relatedId)) {
|
|
|
|
|
record[relationName] = relatedMap.get(relatedId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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('*');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return records;
|
|
|
|
|
// Filter fields based on field-level permissions
|
|
|
|
|
const filteredRecords = await Promise.all(
|
|
|
|
|
records.map(record =>
|
|
|
|
|
this.authService.filterReadableFields(record, objectDefModel.fields, user)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return filteredRecords;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getRecord(
|
|
|
|
|
@@ -634,8 +653,32 @@ export class ObjectService {
|
|
|
|
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
|
|
|
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
|
|
|
|
|
|
|
|
|
// Verify object exists
|
|
|
|
|
await this.getObjectDefinition(tenantId, objectApiName);
|
|
|
|
|
// Get user with roles and permissions
|
|
|
|
|
const user = await User.query(knex)
|
|
|
|
|
.findById(userId)
|
|
|
|
|
.withGraphFetched('[roles.[objectPermissions, fieldPermissions]]');
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if user has create permission
|
|
|
|
|
const canCreate = await this.authService.canCreate(objectDefModel, user);
|
|
|
|
|
if (!canCreate) {
|
|
|
|
|
throw new NotFoundException('You do not have permission to create records of this object');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
|
|
|
|
@@ -646,7 +689,7 @@ export class ObjectService {
|
|
|
|
|
if (Model) {
|
|
|
|
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
|
|
|
|
const recordData = {
|
|
|
|
|
...data,
|
|
|
|
|
...editableData,
|
|
|
|
|
ownerId: userId, // Auto-set owner
|
|
|
|
|
};
|
|
|
|
|
const record = await boundModel.query().insert(recordData);
|
|
|
|
|
@@ -660,9 +703,13 @@ export class ObjectService {
|
|
|
|
|
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: knex.raw('(UUID())'),
|
|
|
|
|
...data,
|
|
|
|
|
id: uuid,
|
|
|
|
|
...editableData,
|
|
|
|
|
created_at: knex.fn.now(),
|
|
|
|
|
updated_at: knex.fn.now(),
|
|
|
|
|
};
|
|
|
|
|
@@ -671,9 +718,9 @@ export class ObjectService {
|
|
|
|
|
recordData.ownerId = userId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [id] = await knex(tableName).insert(recordData);
|
|
|
|
|
await knex(tableName).insert(recordData);
|
|
|
|
|
|
|
|
|
|
return knex(tableName).where({ id }).first();
|
|
|
|
|
return knex(tableName).where({ id: uuid }).first();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateRecord(
|
|
|
|
|
@@ -686,10 +733,43 @@ export class ObjectService {
|
|
|
|
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
|
|
|
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
|
|
|
|
|
|
|
|
|
// Verify object exists and user has access
|
|
|
|
|
await this.getRecord(tenantId, objectApiName, recordId, userId);
|
|
|
|
|
// Get user with roles and permissions
|
|
|
|
|
const user = await User.query(knex)
|
|
|
|
|
.findById(userId)
|
|
|
|
|
.withGraphFetched('[roles.[objectPermissions, fieldPermissions]]');
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tableName = this.getTableName(objectApiName);
|
|
|
|
|
|
|
|
|
|
// Get existing record
|
|
|
|
|
const existingRecord = await knex(tableName).where({ id: recordId }).first();
|
|
|
|
|
if (!existingRecord) {
|
|
|
|
|
throw new NotFoundException('Record not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if user can update this record
|
|
|
|
|
await this.authService.assertCanPerformAction('update', objectDefModel, existingRecord, user, knex);
|
|
|
|
|
|
|
|
|
|
// Filter data to only editable fields
|
|
|
|
|
const editableData = await this.authService.filterEditableFields(data, objectDefModel.fields, user);
|
|
|
|
|
|
|
|
|
|
// Remove system fields
|
|
|
|
|
delete editableData.id;
|
|
|
|
|
delete editableData.ownerId;
|
|
|
|
|
delete editableData.created_at;
|
|
|
|
|
delete editableData.tenantId;
|
|
|
|
|
|
|
|
|
|
// Ensure model is registered before attempting to use it
|
|
|
|
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
|
|
|
|
@@ -699,14 +779,7 @@ export class ObjectService {
|
|
|
|
|
const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
|
|
|
|
|
if (Model) {
|
|
|
|
|
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
|
|
|
|
|
// Don't allow updating ownerId or system fields
|
|
|
|
|
const allowedData = { ...data };
|
|
|
|
|
delete allowedData.ownerId;
|
|
|
|
|
delete allowedData.id;
|
|
|
|
|
delete allowedData.created_at;
|
|
|
|
|
delete allowedData.tenantId;
|
|
|
|
|
|
|
|
|
|
await boundModel.query().where({ id: recordId }).update(allowedData);
|
|
|
|
|
await boundModel.query().where({ id: recordId }).update(editableData);
|
|
|
|
|
return boundModel.query().where({ id: recordId }).first();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
@@ -716,7 +789,7 @@ export class ObjectService {
|
|
|
|
|
// Fallback to raw Knex
|
|
|
|
|
await knex(tableName)
|
|
|
|
|
.where({ id: recordId })
|
|
|
|
|
.update({ ...data, updated_at: knex.fn.now() });
|
|
|
|
|
.update({ ...editableData, updated_at: knex.fn.now() });
|
|
|
|
|
|
|
|
|
|
return knex(tableName).where({ id: recordId }).first();
|
|
|
|
|
}
|
|
|
|
|
@@ -730,10 +803,33 @@ export class ObjectService {
|
|
|
|
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
|
|
|
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
|
|
|
|
|
|
|
|
|
// Verify object exists and user has access
|
|
|
|
|
await this.getRecord(tenantId, objectApiName, recordId, userId);
|
|
|
|
|
// Get user with roles and permissions
|
|
|
|
|
const user = await User.query(knex)
|
|
|
|
|
.findById(userId)
|
|
|
|
|
.withGraphFetched('[roles.[objectPermissions, fieldPermissions]]');
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get object definition with authorization settings
|
|
|
|
|
const objectDefModel = await ObjectDefinition.query(knex)
|
|
|
|
|
.findOne({ apiName: objectApiName });
|
|
|
|
|
|
|
|
|
|
if (!objectDefModel) {
|
|
|
|
|
throw new NotFoundException(`Object ${objectApiName} not found`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tableName = this.getTableName(objectApiName);
|
|
|
|
|
|
|
|
|
|
// Get existing record
|
|
|
|
|
const existingRecord = await knex(tableName).where({ id: recordId }).first();
|
|
|
|
|
if (!existingRecord) {
|
|
|
|
|
throw new NotFoundException('Record not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
await this.ensureModelRegistered(resolvedTenantId, objectApiName);
|
|
|
|
|
|