feat(query): validate JSON Logic operators and improve error handling
- Add validation for JSON Logic operators in query filters - Throw InvalidQueryOperatorError for invalid operators - Improve error handling and logging in applyJsonLogicFilter - Update tests to verify new error handling behavior - Fix TypeScript linting issues and improve code style
This commit is contained in:
parent
51d336b88b
commit
4b750c593d
@ -331,16 +331,28 @@ describe('Query Engine', () => {
|
||||
expect(Object.keys(result.entities)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles malformed JSON Logic expressions', async () => {
|
||||
it('rejects invalid JSON Logic operators', async () => {
|
||||
await createUser('user1', 'Alice', 25);
|
||||
|
||||
// Should throw an error for invalid operator
|
||||
await expect(
|
||||
queryEngine.query('user', {
|
||||
'invalid-operator': [{ 'var': 'age' }, 25]
|
||||
})
|
||||
).rejects.toThrow('Invalid query operator: invalid-operator');
|
||||
});
|
||||
|
||||
it('handles valid JSON Logic expressions with runtime errors', async () => {
|
||||
await createUser('user1', 'Alice', 25);
|
||||
|
||||
// This is a valid operator but will cause a runtime error due to type mismatch
|
||||
const result = await queryEngine.query('user', {
|
||||
'invalid-operator': [{ 'var': 'age' }, 25]
|
||||
'>': [{ 'var': 'name' }, 25] // Can't compare string and number with >
|
||||
});
|
||||
|
||||
// Should not crash, may return empty results or skip problematic entities
|
||||
// Should still return a result but log the error
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.totalFound).toBe('number');
|
||||
expect(result.totalFound).toBe(0); // No matches due to the error
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { apply } from 'json-logic-js';
|
||||
import { apply, is_logic } from 'json-logic-js';
|
||||
import Debug from 'debug';
|
||||
import { SchemaRegistry, SchemaID, ObjectSchema } from '../schema/schema';
|
||||
import { Lossless, LosslessViewOne, LosslessViewMany, CollapsedDelta } from '../views/lossless';
|
||||
@ -7,6 +7,21 @@ import { DeltaFilter } from '../core/delta';
|
||||
|
||||
const debug = Debug('rz:query');
|
||||
|
||||
// List of valid JSON Logic operators
|
||||
const VALID_OPERATORS = new Set([
|
||||
'==', '===', '!=', '!==', '>', '>=', '<', '<=', '!', '!!',
|
||||
'and', 'or', 'if', '?:', '??', '!!', '!', '!!', '!!', '!',
|
||||
'var', 'missing', 'missing_some', 'in', 'cat', 'log', 'method', 'merge',
|
||||
'+', '-', '*', '/', '%', 'min', 'max', 'map', 'reduce', 'filter', 'all', 'some', 'none'
|
||||
]);
|
||||
|
||||
class InvalidQueryOperatorError extends Error {
|
||||
constructor(operator: string) {
|
||||
super(`Invalid query operator: ${operator}`);
|
||||
this.name = 'InvalidQueryOperatorError';
|
||||
}
|
||||
}
|
||||
|
||||
export type JsonLogic = Record<string, unknown>;
|
||||
|
||||
export interface QueryOptions {
|
||||
@ -29,12 +44,52 @@ export class QueryEngine {
|
||||
/**
|
||||
* Query entities by schema type with optional JSON Logic filter
|
||||
*/
|
||||
/**
|
||||
* Validate JSON Logic operators in a filter
|
||||
* @throws {InvalidQueryOperatorError} If an invalid operator is found
|
||||
*/
|
||||
private validateJsonLogicOperators(logic: unknown): void {
|
||||
if (!logic || typeof logic !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const logicObj = logic as Record<string, unknown>;
|
||||
const operator = Object.keys(logicObj)[0];
|
||||
|
||||
// Check if this is an operator
|
||||
if (is_logic(logic) && operator && !VALID_OPERATORS.has(operator)) {
|
||||
throw new InvalidQueryOperatorError(operator);
|
||||
}
|
||||
|
||||
// Recursively check nested logic
|
||||
for (const value of Object.values(logicObj)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => this.validateJsonLogicOperators(item));
|
||||
} else if (value && typeof value === 'object') {
|
||||
this.validateJsonLogicOperators(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async query(
|
||||
schemaId: SchemaID,
|
||||
filter?: JsonLogic,
|
||||
options: QueryOptions = {}
|
||||
): Promise<QueryResult> {
|
||||
debug(`Querying schema ${schemaId} with filter:`, filter);
|
||||
|
||||
// Validate filter operators if provided
|
||||
if (filter) {
|
||||
try {
|
||||
this.validateJsonLogicOperators(filter);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidQueryOperatorError) {
|
||||
debug(`Invalid query operator: ${error.message}`);
|
||||
throw error; // Re-throw to let the caller handle it
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Find all entities that could match this schema
|
||||
const candidateEntityIds = this.discoverEntitiesBySchema(schemaId);
|
||||
@ -154,12 +209,14 @@ export class QueryEngine {
|
||||
}
|
||||
|
||||
const filteredViews: LosslessViewMany = {};
|
||||
let hasFilterErrors = false;
|
||||
const filterErrors: string[] = [];
|
||||
|
||||
for (const [entityId, view] of Object.entries(views)) {
|
||||
// Convert lossless view to queryable object using schema
|
||||
const queryableObject = this.losslessViewToQueryableObject(view, schema);
|
||||
|
||||
try {
|
||||
// Convert lossless view to queryable object using schema
|
||||
const queryableObject = this.losslessViewToQueryableObject(view, schema);
|
||||
|
||||
// Apply JSON Logic filter
|
||||
const matches = apply(filter, queryableObject);
|
||||
|
||||
@ -170,11 +227,20 @@ export class QueryEngine {
|
||||
debug(`Entity ${entityId} does not match filter`);
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Error applying filter to entity ${entityId}:`, error);
|
||||
// Skip entities that cause filter errors
|
||||
hasFilterErrors = true;
|
||||
const errorMsg = `Error applying filter to entity ${entityId}: ${error instanceof Error ? error.message : String(error)}`;
|
||||
filterErrors.push(errorMsg);
|
||||
debug(errorMsg, error);
|
||||
// Continue processing other entities
|
||||
}
|
||||
}
|
||||
|
||||
// If we had any filter errors, log them as a warning
|
||||
if (hasFilterErrors) {
|
||||
console.warn(`Encountered ${filterErrors.length} filter errors. First error: ${filterErrors[0]}`);
|
||||
debug('All filter errors:', filterErrors);
|
||||
}
|
||||
|
||||
return filteredViews;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user