UNPKG

odata-active-record-core

Version:

Core Active Record implementation for OData - The easiest way to interact with OData APIs

431 lines 13.4 kB
/** * ActiveRecord class - The main class for OData Active Record pattern * Provides fluent query interface with seamless data type handling */ export class ActiveRecord { schema; dataTypeHandler; query = {}; warnings = []; errors = []; constructor(schema, dataTypeHandler) { this.schema = schema; this.dataTypeHandler = dataTypeHandler; } /** * Add a where condition to the query */ where(field, operator, value) { if (!this.validateField(field)) { this.addError({ code: 'INVALID_FIELD', message: `Field '${String(field)}' does not exist in schema`, suggestion: `Available fields: ${Object.keys(this.schema.fields).join(', ')}`, severity: 'error', actionable: true, field: String(field) }); return this; } // Validate operator const validOperators = ['eq', 'ne', 'lt', 'le', 'gt', 'ge', 'in', 'contains', 'startswith', 'endswith']; if (!validOperators.includes(operator)) { this.addError({ code: 'INVALID_OPERATOR', message: `Invalid operator '${operator}'`, suggestion: `Valid operators: ${validOperators.join(', ')}`, severity: 'error', actionable: true }); return this; } // Auto-convert value based on field type const fieldDef = this.schema.fields[field]; const convertedValue = this.dataTypeHandler.autoConvert(value, fieldDef.type); const filter = { field: String(field), operator: operator, value: convertedValue }; if (!this.query.filter) { this.query.filter = filter; } else { // Combine with existing filter using AND this.query.filter = { field: '', operator: 'eq', value: null, logicalOperator: 'and', children: [this.query.filter, filter] }; } return this; } /** * Select specific fields */ select(...fields) { // Validate all fields exist const invalidFields = fields.filter(field => !this.validateField(field)); if (invalidFields.length > 0) { this.addError({ code: 'INVALID_FIELDS', message: `Invalid fields: ${invalidFields.join(', ')}`, suggestion: `Available fields: ${Object.keys(this.schema.fields).join(', ')}`, severity: 'error', actionable: true }); return this; } this.query.select = { fields: fields.map(f => String(f)), exclude: false }; return this; } /** * Order by a field */ orderBy(field, direction = 'asc') { if (!this.validateField(field)) { this.addError({ code: 'INVALID_FIELD', message: `Field '${String(field)}' does not exist in schema`, suggestion: `Available fields: ${Object.keys(this.schema.fields).join(', ')}`, severity: 'error', actionable: true, field: String(field) }); return this; } const order = { field: String(field), direction }; if (!this.query.orderBy) { this.query.orderBy = []; } this.query.orderBy.push(order); return this; } /** * Limit the number of results */ limit(count) { if (!this.query.pagination) { this.query.pagination = {}; } this.query.pagination.take = count; return this; } /** * Skip a number of results */ offset(count) { if (!this.query.pagination) { this.query.pagination = {}; } this.query.pagination.skip = count; return this; } /** * Expand a relationship */ expand(relation, callback) { if (!this.query.expand) { this.query.expand = []; } const expandQuery = { relation, single: false }; if (callback) { // Create a new ActiveRecord instance for the nested query const nestedActiveRecord = new ActiveRecord({}, this.dataTypeHandler); callback(nestedActiveRecord); // Note: In a real implementation, we would need to return the ActiveRecord instance itself // as it implements IQueryBuilder, but for now we'll omit the nestedQuery property } this.query.expand.push(expandQuery); return this; } /** * Execute the query and return results */ async find() { try { // For now, return a mock result // In a real implementation, this would execute against a database const mockData = []; const executionTime = Math.random() * 100; // Mock execution time return { data: mockData, success: this.errors.length === 0, errors: this.errors.length > 0 ? this.errors : [], warnings: this.warnings.length > 0 ? this.warnings : [], metadata: { count: mockData.length, executionTime, cacheStatus: 'miss' } }; } catch (error) { return { data: [], success: false, errors: [this.createUserFriendlyError(error)], metadata: { count: 0, executionTime: 0, cacheStatus: 'miss' } }; } } /** * Execute the query and return a single result */ async findOne() { const result = await this.find(); if (result.success && result.data.length > 0) { return result.data[0] || null; } return null; } /** * Execute a count query */ async count() { const result = await this.find(); return result.metadata.count; } /** * Create a new entity */ create(data) { try { const convertedData = this.convertDataTypes(data); const validationResult = this.validateData(convertedData); if (!validationResult.isValid) { return { success: false, errors: validationResult.errors, metadata: { created: false, executionTime: 0 } }; } // Mock creation - in real implementation, this would save to database const createdEntity = { ...convertedData, id: Math.floor(Math.random() * 1000000) // Mock ID }; return { data: createdEntity, id: createdEntity.id, success: true, metadata: { created: true, executionTime: Math.random() * 50 } }; } catch (error) { return { success: false, errors: [this.createUserFriendlyError(error)], metadata: { created: false, executionTime: 0 } }; } } /** * Update an entity */ update(id, data) { try { const convertedData = this.convertDataTypes(data); const validationResult = this.validateData(convertedData); if (!validationResult.isValid) { return { success: false, errors: validationResult.errors, metadata: { updated: false, affectedCount: 0, executionTime: 0 } }; } // Mock update - in real implementation, this would update in database const updatedEntity = { id, ...convertedData }; return { data: updatedEntity, success: true, metadata: { updated: true, affectedCount: 1, executionTime: Math.random() * 50 } }; } catch (error) { return { success: false, errors: [this.createUserFriendlyError(error)], metadata: { updated: false, affectedCount: 0, executionTime: 0 } }; } } /** * Delete an entity */ delete(id) { try { // Mock deletion - in real implementation, this would delete from database return { success: true, metadata: { deleted: true, affectedCount: 1, executionTime: Math.random() * 30 } }; } catch (error) { return { success: false, errors: [this.createUserFriendlyError(error)], metadata: { deleted: false, affectedCount: 0, executionTime: 0 } }; } } /** * Validate if a field exists in the schema */ validateField(field) { return field in this.schema.fields; } /** * Get the schema */ getSchema() { return this.schema; } /** * Get warnings */ getWarnings() { return this.warnings; } /** * Get the built query */ buildQuery() { return this.query; } /** * Convert data types based on schema */ convertDataTypes(data) { const converted = {}; for (const [key, value] of Object.entries(data)) { const fieldDef = this.schema.fields[key]; if (fieldDef) { const convertedValue = this.dataTypeHandler.autoConvert(value, fieldDef.type); converted[key] = convertedValue; } } return converted; } /** * Validate data against schema */ validateData(data) { const errors = []; const warnings = []; for (const [key, value] of Object.entries(data)) { const fieldDef = this.schema.fields[key]; if (!fieldDef) { errors.push({ code: 'INVALID_FIELD', message: `Field '${key}' does not exist in schema`, suggestion: `Available fields: ${Object.keys(this.schema.fields).join(', ')}`, severity: 'error', actionable: true, field: key }); continue; } // Check required fields if (!fieldDef.nullable && (value === null || value === undefined || value === '')) { errors.push({ code: 'REQUIRED_FIELD', message: `Field '${key}' is required`, suggestion: `Provide a value for the ${key} field`, severity: 'error', actionable: true, field: key }); } // Check email validation if (fieldDef.type === 'string' && key.toLowerCase().includes('email')) { if (typeof value === 'string' && !this.dataTypeHandler.validateEmail(value)) { errors.push({ code: 'INVALID_EMAIL', message: `Invalid email format for field '${key}'`, suggestion: 'Please provide a valid email address', severity: 'error', actionable: true, field: key }); } } } return { isValid: errors.length === 0, errors, warnings, suggestions: errors.map(e => e.suggestion).filter(Boolean) }; } /** * Add an error to the error collection */ addError(error) { this.errors.push(error); } /** * Create a user-friendly error from a raw error */ createUserFriendlyError(error) { return { code: 'INTERNAL_ERROR', message: error.message, suggestion: 'Please try again or contact support if the problem persists', severity: 'error', actionable: false, context: { errorType: error.constructor.name, stack: error.stack } }; } } //# sourceMappingURL=active-record.js.map