WIP - more progress with permissions

This commit is contained in:
Francisco Gaona
2025-12-28 06:48:03 +01:00
parent 88f656c3f5
commit ac4a4b68cd
8 changed files with 333 additions and 91 deletions

View File

@@ -49,7 +49,9 @@ export class DynamicModelFactory {
updated_at: { type: 'string', format: 'date-time' }, updated_at: { type: 'string', format: 'date-time' },
}; };
const required: string[] = ['id', 'tenantId']; // Don't require system-managed fields (id, tenantId, ownerId, timestamps)
// These are auto-set by hooks or database
const required: string[] = [];
// Add custom fields // Add custom fields
for (const field of fields) { for (const field of fields) {
@@ -134,15 +136,16 @@ 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(opt: any, queryContext: any) {
this.updated_at = new Date().toISOString(); await super.$beforeUpdate(opt, queryContext);
this.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' ');
} }
} }

View File

@@ -1,4 +1,4 @@
import { Injectable, NotFoundException, Logger } from '@nestjs/common'; import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
import { TenantDatabaseService } from '../tenant/tenant-database.service'; import { TenantDatabaseService } from '../tenant/tenant-database.service';
import { CustomMigrationService } from '../migration/custom-migration.service'; import { CustomMigrationService } from '../migration/custom-migration.service';
import { ModelService } from './models/model.service'; import { ModelService } from './models/model.service';
@@ -350,6 +350,53 @@ export class ObjectService {
return typeMap[frontendType] || 'TEXT'; return typeMap[frontendType] || 'TEXT';
} }
/**
* Filter incoming data to only include writable fields based on field definitions
* Removes system fields and fields that don't exist in the schema
*/
private async filterWritableFields(
tenantId: string,
objectApiName: string,
data: any,
isUpdate: boolean = false,
): Promise<any> {
const objectDef = await this.getObjectDefinition(tenantId, objectApiName);
const filtered: any = {};
for (const [key, value] of Object.entries(data)) {
// Find the field definition
const fieldDef = objectDef.fields.find((f: any) => f.apiName === key);
if (!fieldDef) {
// Field doesn't exist in schema, skip it
this.logger.warn(`Field ${key} not found in ${objectApiName} schema, skipping`);
continue;
}
// Skip system fields
if (fieldDef.isSystem) {
this.logger.debug(`Skipping system field ${key}`);
continue;
}
// Check if field is writable (for authorization)
if (fieldDef.defaultWritable === false) {
this.logger.warn(`Field ${key} is not writable, skipping`);
continue;
}
// For update operations, also skip ID field
if (isUpdate && key === 'id') {
continue;
}
// Field is valid and writable, include it
filtered[key] = value;
}
return filtered;
}
/** /**
* Ensure a model is registered for the given object. * Ensure a model is registered for the given object.
* Delegates to ModelService which handles creating the model and all its dependencies. * Delegates to ModelService which handles creating the model and all its dependencies.
@@ -553,6 +600,21 @@ export class ObjectService {
// Verify object exists and get field definitions // Verify object exists and get field definitions
const objectDef = await this.getObjectDefinition(tenantId, objectApiName); const objectDef = await this.getObjectDefinition(tenantId, objectApiName);
// Get object definition with authorization settings
const objectDefModel = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
if (!objectDefModel) {
throw new NotFoundException('Object definition not found');
}
// Get user model for authorization
const user = await User.query(knex).findById(userId).withGraphFetched('roles');
if (!user) {
throw new NotFoundException('User not found');
}
const tableName = this.getTableName(objectApiName); const tableName = this.getTableName(objectApiName);
// Ensure model is registered before attempting to use it // Ensure model is registered before attempting to use it
@@ -565,6 +627,9 @@ export class ObjectService {
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
let query = boundModel.query().where({ id: recordId }); let query = boundModel.query().where({ id: recordId });
// Apply authorization scoping
query = applyReadScope(query, user, objectDefModel, knex);
// Build graph expression for lookup fields // Build graph expression for lookup fields
const lookupFields = objectDef.fields?.filter(f => const lookupFields = objectDef.fields?.filter(f =>
f.type === 'LOOKUP' && f.referenceObject f.type === 'LOOKUP' && f.referenceObject
@@ -582,15 +647,9 @@ 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 });
}
const record = await query.first(); const record = await query.first();
if (!record) { if (!record) {
throw new NotFoundException('Record not found'); throw new NotFoundException('Record not found or you do not have access');
} }
return record; return record;
} }
@@ -598,10 +657,10 @@ export class ObjectService {
this.logger.warn(`Could not use Objection model for ${objectApiName}, falling back to manual join: ${error.message}`); this.logger.warn(`Could not use Objection model for ${objectApiName}, falling back to manual join: ${error.message}`);
} }
// Fallback to manual data hydration // Fallback to manual data hydration - Note: This path doesn't support authorization scoping yet
let query = knex(tableName).where({ [`${tableName}.id`]: recordId }); let query = knex(tableName).where({ [`${tableName}.id`]: recordId });
// Add ownership filter if ownerId field exists // Add ownership filter if ownerId field exists (basic fallback)
const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId'); const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId');
if (hasOwner) { if (hasOwner) {
query = query.where({ [`${tableName}.ownerId`]: userId }); query = query.where({ [`${tableName}.ownerId`]: userId });
@@ -610,7 +669,7 @@ export class ObjectService {
const record = await query.first(); const record = await query.first();
if (!record) { if (!record) {
throw new NotFoundException('Record not found'); throw new NotFoundException('Record not found or you do not have access');
} }
// Fetch and attach related records for lookup fields // Fetch and attach related records for lookup fields
@@ -652,6 +711,32 @@ export class ObjectService {
// Verify object exists // Verify object exists
await this.getObjectDefinition(tenantId, objectApiName); await this.getObjectDefinition(tenantId, objectApiName);
// Get object definition with authorization settings
const objectDefModel = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
if (!objectDefModel) {
throw new NotFoundException('Object definition not found');
}
// Check create permission
if (!objectDefModel.publicCreate) {
// Get user with roles to check role-based permissions
const user = await User.query(knex).findById(userId).withGraphFetched('roles');
if (!user) {
throw new NotFoundException('User not found');
}
// TODO: Check role-based create permissions from role_rules
// For now, only allow if publicCreate is true
throw new ForbiddenException('You do not have permission to create records for this object');
}
// Filter data to only include writable fields based on field definitions
// Do this BEFORE model registration so both Objection and fallback paths use clean data
const allowedData = await this.filterWritableFields(tenantId, objectApiName, data, false);
// Ensure model is registered before attempting to use it // Ensure model is registered before attempting to use it
await this.ensureModelRegistered(resolvedTenantId, objectApiName); await this.ensureModelRegistered(resolvedTenantId, objectApiName);
@@ -660,8 +745,9 @@ export class ObjectService {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
if (Model) { if (Model) {
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
const recordData = { const recordData = {
...data, ...allowedData,
ownerId: userId, // Auto-set owner ownerId: userId, // Auto-set owner
}; };
const record = await boundModel.query().insert(recordData); const record = await boundModel.query().insert(recordData);
@@ -677,7 +763,7 @@ export class ObjectService {
const recordData: any = { const recordData: any = {
id: knex.raw('(UUID())'), id: knex.raw('(UUID())'),
...data, ...allowedData, // Use filtered data instead of raw data
created_at: knex.fn.now(), created_at: knex.fn.now(),
updated_at: knex.fn.now(), updated_at: knex.fn.now(),
}; };
@@ -701,37 +787,65 @@ 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 user has access // Get object definition with authorization settings
await this.getRecord(tenantId, objectApiName, recordId, userId); const objectDefModel = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
if (!objectDefModel) {
throw new NotFoundException('Object definition not found');
}
// Get user model for authorization
const user = await User.query(knex).findById(userId).withGraphFetched('roles');
if (!user) {
throw new NotFoundException('User not found');
}
// Filter data to only include writable fields based on field definitions
// Do this BEFORE authorization checks so both paths use clean data
const allowedData = await this.filterWritableFields(tenantId, objectApiName, data, true);
// Verify user has access to read the record first (using authorization scope)
const tableName = this.getTableName(objectApiName); const tableName = this.getTableName(objectApiName);
// Ensure model is registered before attempting to use it
await this.ensureModelRegistered(resolvedTenantId, objectApiName); await this.ensureModelRegistered(resolvedTenantId, objectApiName);
// Try to use the Objection model if available
try {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
if (Model) { if (Model) {
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
// Don't allow updating ownerId or system fields let checkQuery = boundModel.query().where({ id: recordId });
const allowedData = { ...data }; checkQuery = applyUpdateScope(checkQuery, user, objectDefModel, knex);
delete allowedData.ownerId;
delete allowedData.id;
delete allowedData.created_at;
delete allowedData.tenantId;
await boundModel.query().where({ id: recordId }).update(allowedData); const existingRecord = await checkQuery.first();
return boundModel.query().where({ id: recordId }).first(); if (!existingRecord) {
} throw new ForbiddenException('You do not have permission to update this record');
} catch (error) { }
console.warn(`Could not use Objection model for ${objectApiName}:`, error.message);
this.logger.log(`[UPDATE] Record ID: ${recordId}, Type: ${typeof recordId}`);
this.logger.log(`[UPDATE] Existing record ID: ${existingRecord.id}, Type: ${typeof existingRecord.id}`);
this.logger.log(`[UPDATE] Allowed data:`, JSON.stringify(allowedData));
const numUpdated = await boundModel.query().where({ id: recordId }).update(allowedData);
this.logger.log(`[UPDATE] Number of records updated: ${numUpdated}`);
const updatedRecord = await boundModel.query().where({ id: recordId }).first();
this.logger.log(`[UPDATE] Updated record:`, updatedRecord ? 'found' : 'NOT FOUND');
return updatedRecord;
}
// Fallback to raw Knex with basic ownership check
const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId');
if (hasOwner && !objectDefModel.publicUpdate) {
const record = await knex(tableName).where({ id: recordId, ownerId: userId }).first();
if (!record) {
throw new ForbiddenException('You do not have permission to update this record');
}
} }
// Fallback to raw Knex
await knex(tableName) await knex(tableName)
.where({ id: recordId }) .where({ id: recordId })
.update({ ...data, updated_at: knex.fn.now() }); .update({ ...allowedData, updated_at: knex.fn.now() }); // Use filtered data
return knex(tableName).where({ id: recordId }).first(); return knex(tableName).where({ id: recordId }).first();
} }
@@ -745,27 +859,51 @@ 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 user has access // Get object definition with authorization settings
await this.getRecord(tenantId, objectApiName, recordId, userId); const objectDefModel = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
if (!objectDefModel) {
throw new NotFoundException('Object definition not found');
}
// Get user model for authorization
const user = await User.query(knex).findById(userId).withGraphFetched('roles');
if (!user) {
throw new NotFoundException('User not found');
}
const tableName = this.getTableName(objectApiName); const tableName = this.getTableName(objectApiName);
// Ensure model is registered before attempting to use it
await this.ensureModelRegistered(resolvedTenantId, objectApiName); await this.ensureModelRegistered(resolvedTenantId, objectApiName);
// Try to use the Objection model if available // Try to use the Objection model if available
try {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
if (Model) { if (Model) {
const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName);
// Check if user has permission to delete this record
let checkQuery = boundModel.query().where({ id: recordId });
checkQuery = applyDeleteScope(checkQuery, user, objectDefModel, knex);
const existingRecord = await checkQuery.first();
if (!existingRecord) {
throw new ForbiddenException('You do not have permission to delete this record');
}
await boundModel.query().where({ id: recordId }).delete(); await boundModel.query().where({ id: recordId }).delete();
return { success: true }; return { success: true };
} }
} catch (error) {
console.warn(`Could not use Objection model for ${objectApiName}:`, error.message); // Fallback to raw Knex with basic ownership check
const hasOwner = await knex.schema.hasColumn(tableName, 'ownerId');
if (hasOwner && !objectDefModel.publicDelete) {
const record = await knex(tableName).where({ id: recordId, ownerId: userId }).first();
if (!record) {
throw new ForbiddenException('You do not have permission to delete this record');
}
} }
// Fallback to raw Knex
await knex(tableName).where({ id: recordId }).delete(); await knex(tableName).where({ id: recordId }).delete();
return { success: true }; return { success: true };

View File

@@ -12,6 +12,8 @@ import {
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
console.log('[EditView] COMPONENT MOUNTING')
interface Props { interface Props {
config: EditViewConfig config: EditViewConfig
data?: any data?: any
@@ -25,6 +27,8 @@ const props = withDefaults(defineProps<Props>(), {
saving: false, saving: false,
}) })
console.log('[EditView] Props received on mount:', JSON.stringify(props, null, 2))
const emit = defineEmits<{ const emit = defineEmits<{
'save': [data: any] 'save': [data: any]
'cancel': [] 'cancel': []
@@ -35,10 +39,16 @@ const emit = defineEmits<{
const formData = ref<Record<string, any>>({ ...props.data }) const formData = ref<Record<string, any>>({ ...props.data })
const errors = ref<Record<string, string>>({}) const errors = ref<Record<string, string>>({})
console.log('[EditView] Initial props.data:', JSON.stringify(props.data, null, 2))
console.log('[EditView] props.data.id:', props.data?.id)
// Watch for data changes (useful for edit mode) // Watch for data changes (useful for edit mode)
watch(() => props.data, (newData) => { watch(() => props.data, (newData) => {
console.log('[EditView] Data changed:', JSON.stringify(newData, null, 2))
console.log('[EditView] newData.id:', newData?.id)
console.log('[EditView] Keys in newData:', Object.keys(newData))
formData.value = { ...newData } formData.value = { ...newData }
}, { deep: true }) }, { deep: true, immediate: true })
// Organize fields into sections // Organize fields into sections
const sections = computed<FieldSection[]>(() => { const sections = computed<FieldSection[]>(() => {
@@ -137,7 +147,11 @@ const validateForm = (): boolean => {
const handleSave = () => { const handleSave = () => {
if (validateForm()) { if (validateForm()) {
emit('save', { ...formData.value }) // Preserve id and other system fields from original data when saving
emit('save', {
id: props.data?.id, // Preserve the record ID for updates
...formData.value
})
} }
} }

View File

@@ -45,11 +45,16 @@ const errors = ref<Record<string, string>>({})
// Watch for data changes (useful for edit mode) // Watch for data changes (useful for edit mode)
watch(() => props.data, (newData) => { watch(() => props.data, (newData) => {
console.log('[EditViewEnhanced] Data changed:', newData)
console.log('[EditViewEnhanced] Data has id?', newData?.id)
formData.value = { ...newData } formData.value = { ...newData }
}, { deep: true }) }, { deep: true, immediate: true })
// Fetch page layout if objectId is provided // Fetch page layout if objectId is provided
onMounted(async () => { onMounted(async () => {
console.log('[EditViewEnhanced] Component mounted')
console.log('[EditViewEnhanced] Props:', props)
if (props.objectId) { if (props.objectId) {
try { try {
loadingLayout.value = true loadingLayout.value = true
@@ -159,13 +164,27 @@ const validateForm = (): boolean => {
} }
const handleSave = () => { const handleSave = () => {
console.log('[EditViewEnhanced] handleSave called')
console.log('[EditViewEnhanced] props.data:', props.data)
console.log('[EditViewEnhanced] props.data?.id:', props.data?.id)
console.log('[EditViewEnhanced] formData before processing:', { ...formData.value })
if (validateForm()) { if (validateForm()) {
// Filter out system fields from save data // Preserve the id from props.data if it exists (needed for updates)
// Filter out other system fields that are auto-managed
const saveData = { ...formData.value } const saveData = { ...formData.value }
const systemFields = ['id', 'tenantId', 'ownerId', 'created_at', 'updated_at', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] const systemFieldsToRemove = ['tenantId', 'ownerId', 'created_at', 'updated_at', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']
for (const field of systemFields) { for (const field of systemFieldsToRemove) {
delete saveData[field] delete saveData[field]
} }
// Explicitly preserve id if it exists in the original data
if (props.data?.id) {
saveData.id = props.data.id
console.log('[EditViewEnhanced] Preserved id from props:', saveData.id)
}
console.log('[EditViewEnhanced] Final saveData:', saveData)
emit('save', saveData) emit('save', saveData)
} }
} }

View File

@@ -22,6 +22,7 @@ interface Props {
loading?: boolean loading?: boolean
selectable?: boolean selectable?: boolean
baseUrl?: string baseUrl?: string
canCreate?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@@ -29,6 +30,7 @@ const props = withDefaults(defineProps<Props>(), {
loading: false, loading: false,
selectable: false, selectable: false,
baseUrl: '/runtime/objects', baseUrl: '/runtime/objects',
canCreate: true,
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@@ -145,7 +147,7 @@ const handleAction = (actionId: string) => {
</Button> </Button>
<!-- Create --> <!-- Create -->
<Button size="sm" @click="emit('create')"> <Button v-if="props.canCreate" size="sm" @click="emit('create')">
<Plus class="h-4 w-4 mr-2" /> <Plus class="h-4 w-4 mr-2" />
New New
</Button> </Button>

View File

@@ -45,7 +45,9 @@ export const useApi = () => {
toast.error('Your session has expired. Please login again.') toast.error('Your session has expired. Please login again.')
router.push('/login') router.push('/login')
} }
throw new Error('Unauthorized') const error = new Error('Unauthorized')
;(error as any).status = 401
throw error
} }
if (response.status === 403) { if (response.status === 403) {
@@ -59,17 +61,24 @@ export const useApi = () => {
router.push('/login') router.push('/login')
} }
} }
throw new Error('Forbidden') // Don't log 403 errors - create error with status flag
const error = new Error('Forbidden')
;(error as any).status = 403
throw error
} }
if (!response.ok) { if (!response.ok) {
// Try to get error details from response // Try to get error details from response
const text = await response.text() const text = await response.text()
// Only log unexpected errors (not 401 or 403 which are handled above)
if (response.status !== 401 && response.status !== 403) {
console.error('API Error Response:', { console.error('API Error Response:', {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
body: text body: text
}) })
}
let errorMessage = `HTTP error! status: ${response.status}` let errorMessage = `HTTP error! status: ${response.status}`
if (text) { if (text) {

View File

@@ -197,7 +197,10 @@ export const useViewState = <T extends { id?: string }>(
records.value = response.data || response || [] records.value = response.data || response || []
} catch (e: any) { } catch (e: any) {
error.value = e.message error.value = e.message
// Only log unexpected errors (not authorization failures)
if (e.status !== 401 && e.status !== 403) {
console.error('Failed to fetch records:', e) console.error('Failed to fetch records:', e)
}
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -210,9 +213,14 @@ export const useViewState = <T extends { id?: string }>(
const response = await api.get(`${apiEndpoint}/${id}`) const response = await api.get(`${apiEndpoint}/${id}`)
// Handle response - data might be directly in response or in response.data // Handle response - data might be directly in response or in response.data
currentRecord.value = response.data || response currentRecord.value = response.data || response
console.log('[fetchRecord] Fetched record:', JSON.stringify(currentRecord.value, null, 2))
console.log('[fetchRecord] Record has id?', currentRecord.value?.id)
} catch (e: any) { } catch (e: any) {
error.value = e.message error.value = e.message
// Only log unexpected errors (not authorization failures)
if (e.status !== 401 && e.status !== 403) {
console.error('Failed to fetch record:', e) console.error('Failed to fetch record:', e)
}
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -231,7 +239,7 @@ export const useViewState = <T extends { id?: string }>(
return recordData return recordData
} catch (e: any) { } catch (e: any) {
error.value = e.message error.value = e.message
console.error('Failed to create record:', e) // Don't log to console - errors are already handled by useApi and shown via toast
throw e throw e
} finally { } finally {
saving.value = false saving.value = false
@@ -256,7 +264,10 @@ export const useViewState = <T extends { id?: string }>(
return recordData return recordData
} catch (e: any) { } catch (e: any) {
error.value = e.message error.value = e.message
// Only log unexpected errors (not authorization failures)
if (e.status !== 401 && e.status !== 403) {
console.error('Failed to update record:', e) console.error('Failed to update record:', e)
}
throw e throw e
} finally { } finally {
saving.value = false saving.value = false
@@ -274,7 +285,10 @@ export const useViewState = <T extends { id?: string }>(
} }
} catch (e: any) { } catch (e: any) {
error.value = e.message error.value = e.message
// Only log unexpected errors (not authorization failures)
if (e.status !== 401 && e.status !== 403) {
console.error('Failed to delete record:', e) console.error('Failed to delete record:', e)
}
throw e throw e
} finally { } finally {
loading.value = false loading.value = false
@@ -289,7 +303,10 @@ export const useViewState = <T extends { id?: string }>(
records.value = records.value.filter(r => !ids.includes(r.id!)) records.value = records.value.filter(r => !ids.includes(r.id!))
} catch (e: any) { } catch (e: any) {
error.value = e.message error.value = e.message
// Only log unexpected errors (not authorization failures)
if (e.status !== 401 && e.status !== 403) {
console.error('Failed to delete records:', e) console.error('Failed to delete records:', e)
}
throw e throw e
} finally { } finally {
loading.value = false loading.value = false
@@ -312,10 +329,17 @@ export const useViewState = <T extends { id?: string }>(
} }
const handleSave = async (data: T) => { const handleSave = async (data: T) => {
// DEBUG: Check if id is present
console.log('[handleSave] Data received:', JSON.stringify(data, null, 2))
console.log('[handleSave] data.id:', data.id)
console.log('[handleSave] currentRecord.value:', currentRecord.value)
let savedRecord let savedRecord
if (data.id) { if (data.id) {
console.log('[handleSave] Calling updateRecord (PUT)')
savedRecord = await updateRecord(data.id, data) savedRecord = await updateRecord(data.id, data)
} else { } else {
console.log('[handleSave] Calling createRecord (POST) - ID IS MISSING!')
savedRecord = await createRecord(data) savedRecord = await createRecord(data)
} }
return savedRecord return savedRecord

View File

@@ -32,6 +32,7 @@ const view = computed(() => {
// State // State
const objectDefinition = ref<any>(null) const objectDefinition = ref<any>(null)
const objectAccess = ref<any>(null)
const loading = ref(true) const loading = ref(true)
const error = ref<string | null>(null) const error = ref<string | null>(null)
@@ -118,9 +119,23 @@ const detailConfig = computed(() => {
const editConfig = computed(() => { const editConfig = computed(() => {
if (!objectDefinition.value) return null if (!objectDefinition.value) return null
return buildEditViewConfig(objectDefinition.value) const config = buildEditViewConfig(objectDefinition.value)
console.log('[PAGE] editConfig computed:', config ? 'EXISTS' : 'NULL')
return config
}) })
// Debug current view state
watch([view, recordId, editConfig, currentRecord, loading, dataLoading], ([v, rid, ec, cr, l, dl]) => {
console.log('[PAGE] View state changed:')
console.log(' - view:', v)
console.log(' - recordId:', rid)
console.log(' - editConfig exists?', !!ec)
console.log(' - currentRecord exists?', !!cr)
console.log(' - loading:', l)
console.log(' - dataLoading:', dl)
console.log(' - Should show EditView?', (v === 'edit' || rid === 'new') && !!ec)
}, { immediate: true })
// Fetch object definition // Fetch object definition
const fetchObjectDefinition = async () => { const fetchObjectDefinition = async () => {
try { try {
@@ -128,6 +143,21 @@ const fetchObjectDefinition = async () => {
error.value = null error.value = null
const response = await api.get(`/setup/objects/${objectApiName.value}`) const response = await api.get(`/setup/objects/${objectApiName.value}`)
objectDefinition.value = response objectDefinition.value = response
// Fetch access permissions
try {
const accessResponse = await api.get(`/setup/objects/${objectApiName.value}/access`)
objectAccess.value = accessResponse
} catch (e) {
console.warn('Failed to fetch access permissions:', e)
// Set defaults if fetch fails
objectAccess.value = {
publicCreate: true,
publicRead: true,
publicUpdate: true,
publicDelete: true,
}
}
} catch (e: any) { } catch (e: any) {
error.value = e.message || 'Failed to load object definition' error.value = e.message || 'Failed to load object definition'
console.error('Error fetching object definition:', e) console.error('Error fetching object definition:', e)
@@ -261,6 +291,7 @@ onMounted(async () => {
:data="records" :data="records"
:loading="dataLoading" :loading="dataLoading"
:base-url="`/runtime/objects`" :base-url="`/runtime/objects`"
:can-create="objectAccess?.publicCreate !== false"
selectable selectable
@row-click="handleRowClick" @row-click="handleRowClick"
@create="handleCreate" @create="handleCreate"
@@ -282,8 +313,9 @@ onMounted(async () => {
/> />
<!-- Edit View --> <!-- Edit View -->
<div v-else-if="(view === 'edit' || recordId === 'new') && editConfig">
<div v-if="false">DEBUG: EditView should render here. view={{ view }}, recordId={{ recordId }}, editConfig={{ !!editConfig }}, currentRecord={{ !!currentRecord }}</div>
<EditView <EditView
v-else-if="(view === 'edit' || recordId === 'new') && editConfig"
:config="editConfig" :config="editConfig"
:data="currentRecord || {}" :data="currentRecord || {}"
:loading="dataLoading" :loading="dataLoading"
@@ -295,6 +327,7 @@ onMounted(async () => {
@back="handleBack" @back="handleBack"
/> />
</div> </div>
</div>
</NuxtLayout> </NuxtLayout>
</template> </template>