odata-sqlite-expand
Version:
OData v4 $expand functionality for SQLite - JOIN operations with TDD and ISP
146 lines • 6.93 kB
JavaScript
export class ExpandBuilder {
buildExpandClause(expand, baseTable, schemas, relationships) {
const result = {
joins: [],
selectFields: [],
parameters: [],
orderBy: [],
limit: undefined,
offset: undefined
};
for (const expandField of expand) {
this.processExpandField(expandField, baseTable, schemas, relationships, result, '');
}
return result;
}
processExpandField(expandField, currentTable, schemas, relationships, result, prefix) {
// Find the relationship
const relationship = this.findRelationship(currentTable, expandField.path, relationships);
if (!relationship) {
throw new Error(`Relationship "${expandField.path}" not found for table "${currentTable}"`);
}
// Find the target table schema
const targetSchema = this.findSchema(relationship.toTable, schemas);
if (!targetSchema) {
throw new Error(`Schema not found for table "${relationship.toTable}"`);
}
// Generate JOIN clause
const joinClause = this.generateJoinClause(currentTable, relationship, expandField, result);
result.joins.push(joinClause);
// Generate SELECT fields
this.generateSelectFields(relationship.toTable, targetSchema, expandField, result, prefix + (prefix ? '_' : '') + relationship.name);
// Handle ordering
if (expandField.orderBy && expandField.orderBy.length > 0) {
for (const orderBy of expandField.orderBy) {
const orderClause = `${relationship.toTable}.${orderBy.field} ${orderBy.direction.toUpperCase()}`;
result.orderBy.push(orderClause);
}
}
// Handle pagination
if (expandField.top !== undefined) {
result.limit = expandField.top;
}
if (expandField.skip !== undefined) {
result.offset = expandField.skip;
}
// Process nested expands
if (expandField.nested && expandField.nested.length > 0) {
for (const nestedField of expandField.nested) {
this.processExpandField(nestedField, relationship.toTable, schemas, relationships, result, prefix + (prefix ? '_' : '') + relationship.name);
}
}
}
findRelationship(fromTable, relationshipName, relationships) {
return relationships.find(r => r.fromTable === fromTable && r.name === relationshipName);
}
findSchema(tableName, schemas) {
return schemas.find(s => s.name === tableName);
}
generateJoinClause(fromTable, relationship, expandField, result) {
let joinClause = `LEFT JOIN ${relationship.toTable} ON ${fromTable}.${relationship.fromColumn} = ${relationship.toTable}.${relationship.toColumn}`;
// Add filtering to JOIN if specified
if (expandField.filter) {
const filterClause = this.buildFilterClause(expandField.filter, relationship.toTable, result);
if (filterClause) {
joinClause += ` AND ${filterClause}`;
}
}
return joinClause;
}
generateSelectFields(tableName, schema, expandField, result, prefix) {
const fieldsToSelect = expandField.select || schema.columns.map(col => col.name);
// Validate selected fields
for (const field of fieldsToSelect) {
const columnExists = schema.columns.some(col => col.name === field);
if (!columnExists) {
throw new Error(`Field "${field}" not found in table "${tableName}"`);
}
}
// Add selected fields to result
for (const field of fieldsToSelect) {
const alias = `${prefix}_${field}`;
result.selectFields.push(`${tableName}.${field} as ${alias}`);
}
}
buildFilterClause(filter, tableName, result) {
switch (filter.operator) {
case 'eq':
result.parameters.push(filter.value);
return `${tableName}.${filter.field} = ?`;
case 'ne':
result.parameters.push(filter.value);
return `${tableName}.${filter.field} != ?`;
case 'lt':
result.parameters.push(filter.value);
return `${tableName}.${filter.field} < ?`;
case 'le':
result.parameters.push(filter.value);
return `${tableName}.${filter.field} <= ?`;
case 'gt':
result.parameters.push(filter.value);
return `${tableName}.${filter.field} > ?`;
case 'ge':
result.parameters.push(filter.value);
return `${tableName}.${filter.field} >= ?`;
case 'in':
if (Array.isArray(filter.value)) {
const placeholders = filter.value.map(() => '?').join(', ');
result.parameters.push(...filter.value);
return `${tableName}.${filter.field} IN (${placeholders})`;
}
throw new Error('IN operator requires array value');
case 'contains':
result.parameters.push(`%${filter.value}%`);
return `${tableName}.${filter.field} LIKE ?`;
case 'startswith':
result.parameters.push(`${filter.value}%`);
return `${tableName}.${filter.field} LIKE ?`;
case 'endswith':
result.parameters.push(`%${filter.value}`);
return `${tableName}.${filter.field} LIKE ?`;
case 'and':
if (!filter.left || !filter.right) {
throw new Error('AND operator requires left and right operands');
}
const leftClause = this.buildFilterClause(filter.left, tableName, result);
const rightClause = this.buildFilterClause(filter.right, tableName, result);
return `(${leftClause} AND ${rightClause})`;
case 'or':
if (!filter.left || !filter.right) {
throw new Error('OR operator requires left and right operands');
}
const leftOrClause = this.buildFilterClause(filter.left, tableName, result);
const rightOrClause = this.buildFilterClause(filter.right, tableName, result);
return `(${leftOrClause} OR ${rightOrClause})`;
case 'not':
if (!filter.left) {
throw new Error('NOT operator requires left operand');
}
const notClause = this.buildFilterClause(filter.left, tableName, result);
return `NOT (${notClause})`;
default:
throw new Error(`Unsupported operator: ${filter.operator}`);
}
}
}
//# sourceMappingURL=expand-builder.js.map