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);
|
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);
|
await createUser('user1', 'Alice', 25);
|
||||||
|
|
||||||
const result = await queryEngine.query('user', {
|
// Should throw an error for invalid operator
|
||||||
|
await expect(
|
||||||
|
queryEngine.query('user', {
|
||||||
'invalid-operator': [{ 'var': 'age' }, 25]
|
'invalid-operator': [{ 'var': 'age' }, 25]
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Invalid query operator: invalid-operator');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should not crash, may return empty results or skip problematic entities
|
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', {
|
||||||
|
'>': [{ 'var': 'name' }, 25] // Can't compare string and number with >
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still return a result but log the error
|
||||||
expect(result).toBeDefined();
|
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 Debug from 'debug';
|
||||||
import { SchemaRegistry, SchemaID, ObjectSchema } from '../schema/schema';
|
import { SchemaRegistry, SchemaID, ObjectSchema } from '../schema/schema';
|
||||||
import { Lossless, LosslessViewOne, LosslessViewMany, CollapsedDelta } from '../views/lossless';
|
import { Lossless, LosslessViewOne, LosslessViewMany, CollapsedDelta } from '../views/lossless';
|
||||||
@ -7,6 +7,21 @@ import { DeltaFilter } from '../core/delta';
|
|||||||
|
|
||||||
const debug = Debug('rz:query');
|
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 type JsonLogic = Record<string, unknown>;
|
||||||
|
|
||||||
export interface QueryOptions {
|
export interface QueryOptions {
|
||||||
@ -29,6 +44,33 @@ export class QueryEngine {
|
|||||||
/**
|
/**
|
||||||
* Query entities by schema type with optional JSON Logic filter
|
* 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(
|
async query(
|
||||||
schemaId: SchemaID,
|
schemaId: SchemaID,
|
||||||
filter?: JsonLogic,
|
filter?: JsonLogic,
|
||||||
@ -36,6 +78,19 @@ export class QueryEngine {
|
|||||||
): Promise<QueryResult> {
|
): Promise<QueryResult> {
|
||||||
debug(`Querying schema ${schemaId} with filter:`, filter);
|
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
|
// 1. Find all entities that could match this schema
|
||||||
const candidateEntityIds = this.discoverEntitiesBySchema(schemaId);
|
const candidateEntityIds = this.discoverEntitiesBySchema(schemaId);
|
||||||
debug(`Found ${candidateEntityIds.length} candidate entities for schema ${schemaId}`);
|
debug(`Found ${candidateEntityIds.length} candidate entities for schema ${schemaId}`);
|
||||||
@ -154,12 +209,14 @@ export class QueryEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredViews: LosslessViewMany = {};
|
const filteredViews: LosslessViewMany = {};
|
||||||
|
let hasFilterErrors = false;
|
||||||
|
const filterErrors: string[] = [];
|
||||||
|
|
||||||
for (const [entityId, view] of Object.entries(views)) {
|
for (const [entityId, view] of Object.entries(views)) {
|
||||||
|
try {
|
||||||
// Convert lossless view to queryable object using schema
|
// Convert lossless view to queryable object using schema
|
||||||
const queryableObject = this.losslessViewToQueryableObject(view, schema);
|
const queryableObject = this.losslessViewToQueryableObject(view, schema);
|
||||||
|
|
||||||
try {
|
|
||||||
// Apply JSON Logic filter
|
// Apply JSON Logic filter
|
||||||
const matches = apply(filter, queryableObject);
|
const matches = apply(filter, queryableObject);
|
||||||
|
|
||||||
@ -170,11 +227,20 @@ export class QueryEngine {
|
|||||||
debug(`Entity ${entityId} does not match filter`);
|
debug(`Entity ${entityId} does not match filter`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug(`Error applying filter to entity ${entityId}:`, error);
|
hasFilterErrors = true;
|
||||||
// Skip entities that cause filter errors
|
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;
|
return filteredViews;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user