UNPKG

@flavoai/fastfold

Version:

Zero-boilerplate backend for React apps with auto-generated CRUD and declarative security

232 lines 8.84 kB
import { SecurityEnforcer } from '../security'; export class CrudGenerator { db; tables; constructor(db, tables) { this.db = db; this.tables = tables; } /** * Generate CRUD operations for a specific table */ generateTableOperations(tableName) { const tableDefinition = this.tables[tableName]; if (!tableDefinition) { throw new Error(`Table '${tableName}' not found in configuration`); } return { // READ operations findMany: this.createFindManyOperation(tableName, tableDefinition), findOne: this.createFindOneOperation(tableName, tableDefinition), count: this.createCountOperation(tableName, tableDefinition), // WRITE operations create: this.createCreateOperation(tableName, tableDefinition), update: this.createUpdateOperation(tableName, tableDefinition), delete: this.createDeleteOperation(tableName, tableDefinition), }; } /** * Create READ MANY operation */ createFindManyOperation(tableName, table) { return async (params, context) => { // Security check const securityContext = { ...context, operation: 'read', tableName }; const hasAccess = await SecurityEnforcer.checkAccess(table.security, securityContext); if (!hasAccess) { throw SecurityEnforcer.createUnauthorizedError('read', tableName); } // Filter results based on security context for owner-based security const securityParams = this.applySecurityFilters(params, table, context); return await this.db.query(tableName, securityParams); }; } /** * Create READ ONE operation */ createFindOneOperation(tableName, table) { return async (id, context) => { // First fetch the record to check permissions const record = await this.db.query(tableName, { where: { id }, limit: 1 }); if (record.length === 0) { throw new Error(`Record with id ${id} not found in table '${tableName}'`); } // Security check with existing data const securityContext = { ...context, operation: 'read', tableName, existingData: record[0] }; const hasAccess = await SecurityEnforcer.checkAccess(table.security, securityContext); if (!hasAccess) { throw SecurityEnforcer.createUnauthorizedError('read', tableName); } return record[0]; }; } /** * Create COUNT operation */ createCountOperation(tableName, table) { return async (where, context) => { // Security check const securityContext = { ...context, operation: 'read', tableName }; const hasAccess = await SecurityEnforcer.checkAccess(table.security, securityContext); if (!hasAccess) { throw SecurityEnforcer.createUnauthorizedError('read', tableName); } // Apply security filters to where clause const securityWhere = this.applySecurityToWhere(where || {}, table, context); return await this.db.count(tableName, securityWhere); }; } /** * Create CREATE operation */ createCreateOperation(tableName, table) { return async (data, context) => { // Validate data against schema this.validateData(data, table.schema); // Security check const securityContext = { ...context, operation: 'create', tableName, data }; const hasAccess = await SecurityEnforcer.checkAccess(table.security, securityContext); if (!hasAccess) { throw SecurityEnforcer.createUnauthorizedError('create', tableName); } return await this.db.create(tableName, data); }; } /** * Create UPDATE operation */ createUpdateOperation(tableName, table) { return async (id, data, context) => { // Validate partial data against schema this.validateData(data, table.schema, true); // First fetch the existing record const existing = await this.db.query(tableName, { where: { id }, limit: 1 }); if (existing.length === 0) { throw new Error(`Record with id ${id} not found in table '${tableName}'`); } // Security check with existing data const securityContext = { ...context, operation: 'update', tableName, data, existingData: existing[0] }; const hasAccess = await SecurityEnforcer.checkAccess(table.security, securityContext); if (!hasAccess) { throw SecurityEnforcer.createUnauthorizedError('update', tableName); } return await this.db.update(tableName, id, data); }; } /** * Create DELETE operation */ createDeleteOperation(tableName, table) { return async (id, context) => { // First fetch the existing record const existing = await this.db.query(tableName, { where: { id }, limit: 1 }); if (existing.length === 0) { throw new Error(`Record with id ${id} not found in table '${tableName}'`); } // Security check with existing data const securityContext = { ...context, operation: 'delete', tableName, existingData: existing[0] }; const hasAccess = await SecurityEnforcer.checkAccess(table.security, securityContext); if (!hasAccess) { throw SecurityEnforcer.createUnauthorizedError('delete', tableName); } return await this.db.delete(tableName, id); }; } /** * Apply security filters to query parameters */ applySecurityFilters(params, table, context) { // For owner-based security, automatically filter by owner field if (table.security.type === 'owner' && context.user) { const ownerField = table.security.ownerField || 'userId'; const securityWhere = { [ownerField]: context.user.id }; return { ...params, where: { ...params.where, ...securityWhere } }; } return params; } /** * Apply security filters to where clause */ applySecurityToWhere(where, table, context) { // For owner-based security, automatically filter by owner field if (table.security.type === 'owner' && context.user) { const ownerField = table.security.ownerField || 'userId'; return { ...where, [ownerField]: context.user.id }; } return where; } /** * Validate data against table schema */ validateData(data, schema, partial = false) { for (const [field, type] of Object.entries(schema)) { const value = data[field]; // Skip validation for partial updates if field is not provided if (partial && value === undefined) continue; // Check required fields for non-partial operations if (!partial && value === undefined) { throw new Error(`Missing required field: ${field}`); } // Type validation if (value !== undefined && value !== null) { const isValid = this.validateFieldType(value, type); if (!isValid) { throw new Error(`Invalid type for field '${field}'. Expected ${type}, got ${typeof value}`); } } } } /** * Validate individual field type */ validateFieldType(value, expectedType) { switch (expectedType) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number' && !isNaN(value); case 'boolean': return typeof value === 'boolean'; case 'date': return value instanceof Date || !isNaN(Date.parse(value)); case 'json': return true; // Any value can be JSON default: return true; } } } //# sourceMappingURL=generator.js.map