mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
feat: implement mergeFilters function and enhance query object merging
- Added mergeFilters function to combine filter objects with priority handling. - Introduced comprehensive tests for mergeFilters in permissions.spec.ts. - Created query.spec.ts to validate query structure and expression handling. - Enhanced error messages in query.ts for better debugging and clarity.
This commit is contained in:
111
app/src/core/object/query/query.spec.ts
Normal file
111
app/src/core/object/query/query.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { makeValidator, exp, Expression, isPrimitive, type Primitive } from "./query";
|
||||
|
||||
describe("query", () => {
|
||||
test("isPrimitive", () => {
|
||||
expect(isPrimitive(1)).toBe(true);
|
||||
expect(isPrimitive("1")).toBe(true);
|
||||
expect(isPrimitive(true)).toBe(true);
|
||||
expect(isPrimitive(false)).toBe(true);
|
||||
|
||||
// not primitives
|
||||
expect(isPrimitive(null)).toBe(false);
|
||||
expect(isPrimitive(undefined)).toBe(false);
|
||||
expect(isPrimitive([])).toBe(false);
|
||||
expect(isPrimitive({})).toBe(false);
|
||||
expect(isPrimitive(Symbol("test"))).toBe(false);
|
||||
expect(isPrimitive(new Date())).toBe(false);
|
||||
expect(isPrimitive(new Error())).toBe(false);
|
||||
expect(isPrimitive(new Set())).toBe(false);
|
||||
expect(isPrimitive(new Map())).toBe(false);
|
||||
});
|
||||
|
||||
test("strict expression creation", () => {
|
||||
// @ts-expect-error
|
||||
expect(() => exp()).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("")).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("invalid")).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq")).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq", 1)).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq", () => null)).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq", () => null, 1)).toThrow();
|
||||
expect(
|
||||
exp(
|
||||
"$eq",
|
||||
() => true,
|
||||
() => null,
|
||||
),
|
||||
).toBeInstanceOf(Expression);
|
||||
});
|
||||
|
||||
test("$eq is required", () => {
|
||||
expect(() => makeValidator([])).toThrow();
|
||||
expect(() =>
|
||||
makeValidator([
|
||||
exp(
|
||||
"$valid",
|
||||
() => true,
|
||||
() => null,
|
||||
),
|
||||
]),
|
||||
).toThrow();
|
||||
expect(
|
||||
makeValidator([
|
||||
exp(
|
||||
"$eq",
|
||||
() => true,
|
||||
() => null,
|
||||
),
|
||||
]),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
test("validates filter structure", () => {
|
||||
const validator = makeValidator([
|
||||
exp(
|
||||
"$eq",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(e, a) => e === a,
|
||||
),
|
||||
exp(
|
||||
"$like",
|
||||
(v: string) => typeof v === "string",
|
||||
(e, a) => e === a,
|
||||
),
|
||||
]);
|
||||
|
||||
// @ts-expect-error intentionally typed as union of given expression keys
|
||||
expect(validator.expressionKeys).toEqual(["$eq", "$like"]);
|
||||
|
||||
// @ts-expect-error "$and" is not allowed
|
||||
expect(() => validator.convert({ $and: {} })).toThrow();
|
||||
|
||||
// @ts-expect-error "$or" must be an object
|
||||
expect(() => validator.convert({ $or: [] })).toThrow();
|
||||
|
||||
// @ts-expect-error "invalid" is not a valid expression key
|
||||
expect(() => validator.convert({ foo: { invalid: "bar" } })).toThrow();
|
||||
|
||||
// @ts-expect-error "invalid" is not a valid expression key
|
||||
expect(() => validator.convert({ foo: { $invalid: "bar" } })).toThrow();
|
||||
|
||||
// @ts-expect-error "null" is not a valid value
|
||||
expect(() => validator.convert({ foo: null })).toThrow();
|
||||
|
||||
// @ts-expect-error only primitives are allowed for $eq
|
||||
expect(() => validator.convert({ foo: { $eq: [] } })).toThrow();
|
||||
|
||||
// @ts-expect-error only strings are allowed for $like
|
||||
expect(() => validator.convert({ foo: { $like: 1 } })).toThrow();
|
||||
|
||||
expect(validator.convert({ foo: "bar" })).toEqual({ foo: { $eq: "bar" } });
|
||||
expect(validator.convert({ foo: { $eq: "bar" } })).toEqual({ foo: { $eq: "bar" } });
|
||||
expect(validator.convert({ foo: { $like: "bar" } })).toEqual({ foo: { $like: "bar" } });
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PrimaryFieldType } from "core/config";
|
||||
import { getPath, invariant } from "bknd/utils";
|
||||
import { getPath, invariant, isPlainObject } from "bknd/utils";
|
||||
|
||||
export type Primitive = PrimaryFieldType | string | number | boolean;
|
||||
export function isPrimitive(value: any): value is Primitive {
|
||||
@@ -26,6 +26,10 @@ export function exp<const Key, const Expect, CTX = any>(
|
||||
valid: (v: Expect) => boolean,
|
||||
validate: (e: Expect, a: unknown, ctx: CTX) => any,
|
||||
): Expression<Key, Expect, CTX> {
|
||||
invariant(typeof key === "string", "key must be a string");
|
||||
invariant(key[0] === "$", "key must start with '$'");
|
||||
invariant(typeof valid === "function", "valid must be a function");
|
||||
invariant(typeof validate === "function", "validate must be a function");
|
||||
return new Expression(key, valid, validate);
|
||||
}
|
||||
|
||||
@@ -85,13 +89,16 @@ function _convert<Exps extends Expressions>(
|
||||
function validate(key: string, value: any, path: string[] = []) {
|
||||
const exp = getExpression(expressions, key as any);
|
||||
if (exp.valid(value) === false) {
|
||||
throw new Error(`Invalid value at "${[...path, key].join(".")}": ${value}`);
|
||||
throw new Error(
|
||||
`Given value at "${[...path, key].join(".")}" is invalid, got "${JSON.stringify(value)}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries($query)) {
|
||||
// if $or, convert each value
|
||||
if (key === "$or") {
|
||||
invariant(isPlainObject(value), "$or must be an object");
|
||||
newQuery.$or = _convert(value, expressions, [...path, key]);
|
||||
|
||||
// if primitive, assume $eq
|
||||
@@ -100,7 +107,7 @@ function _convert<Exps extends Expressions>(
|
||||
newQuery[key] = { $eq: value };
|
||||
|
||||
// if object, check for expressions
|
||||
} else if (typeof value === "object") {
|
||||
} else if (isPlainObject(value)) {
|
||||
// when object is given, check if all keys are expressions
|
||||
const invalid = Object.keys(value).filter(
|
||||
(f) => !ExpressionConditionKeys.includes(f as any),
|
||||
@@ -114,9 +121,13 @@ function _convert<Exps extends Expressions>(
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid key(s) at "${key}": ${invalid.join(", ")}. Expected expressions.`,
|
||||
`Invalid key(s) at "${key}": ${invalid.join(", ")}. Expected expression key: ${ExpressionConditionKeys.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid value at "${[...path, key].join(".")}", got "${JSON.stringify(value)}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +162,9 @@ function _build<Exps extends Expressions>(
|
||||
throw new Error(`Expression does not exist: "${$op}"`);
|
||||
}
|
||||
if (!exp.valid(expected)) {
|
||||
throw new Error(`Invalid expected value at "${[...path, $op].join(".")}": ${expected}`);
|
||||
throw new Error(
|
||||
`Invalid value at "${[...path, $op].join(".")}", got "${JSON.stringify(expected)}"`,
|
||||
);
|
||||
}
|
||||
return exp.validate(expected, actual, options.exp_ctx);
|
||||
}
|
||||
@@ -191,6 +204,10 @@ function _validate(results: ValidationResults): boolean {
|
||||
}
|
||||
|
||||
export function makeValidator<Exps extends Expressions>(expressions: Exps) {
|
||||
if (!expressions.some((e) => e.key === "$eq")) {
|
||||
throw new Error("'$eq' expression is required");
|
||||
}
|
||||
|
||||
return {
|
||||
convert: (query: FilterQuery<Exps>) => _convert(query, expressions),
|
||||
build: (query: FilterQuery<Exps>, options: BuildOptions) =>
|
||||
|
||||
Reference in New Issue
Block a user