odata-active-record-core
Version:
Core Active Record implementation for OData - The easiest way to interact with OData APIs
431 lines • 13.4 kB
JavaScript
/**
* 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