diff --git a/src/index.ts b/src/index.ts index f536dfb..0e0de18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ export class Guantr< Context extends Record = Record > { private _context: Context = {} as Context; - private _permissions: GuantrAnyPermission[] = []; + private _permissions = new Set(); /** * Initializes a new instance of the Guantr class with an optional context. @@ -58,7 +58,7 @@ export class Guantr< * @return {ReadonlyArray} The permissions of the Guantr instance. */ get permissions(): ReadonlyArray { - return this._permissions; + return [...this._permissions]; } /** @@ -97,15 +97,15 @@ export class Guantr< ) => void, ) => void ): void { - this._permissions = [] + this._permissions.clear() callback( - (action, resource) => this._permissions.push({ + (action, resource) => this._permissions.add({ action, resource: typeof resource === 'string' ? resource : resource[0], condition: typeof resource === 'string' ? null : resource[1] as GuantrAnyPermission['condition'], inverted: false }), - (action, resource) => this._permissions.push({ + (action, resource) => this._permissions.add({ action, resource: typeof resource === 'string' ? resource : resource[0], condition: typeof resource === 'string' ? null : resource[1] as GuantrAnyPermission['condition'], @@ -120,7 +120,7 @@ export class Guantr< * @param {GuantrPermission[]} permissions - The array of permissions to set. */ setPermissions(permissions: GuantrPermission[]): void { - this._permissions = permissions as GuantrAnyPermission[]; + this._permissions = new Set(permissions as GuantrAnyPermission[]) } /** @@ -157,18 +157,21 @@ export class Guantr< return this.permissions.some(item => item.action === action && item.resource === resource && !item.inverted) } const relatedPermissions = this.relatedPermissionsFor(action, resource[0]) - if (relatedPermissions.length === 0) return false + .map(permission => ({ ...permission, condition: this.applyContextualOperands(permission.condition) })) + + if (relatedPermissions.length === 0) { + return false + } const passed: boolean[] = [] const passedInverted: boolean[] = [] - for (const permission of relatedPermissions) { if (!permission.condition) { if (permission.inverted) passedInverted.push(false) else passed.push(true) continue } - const matched = matchPermissionCondition(resource[1], permission.condition, this.context) + const matched = matchPermissionCondition(resource[1], permission.condition) if (matched) { if (permission.inverted) passedInverted.push(false) else passed.push(true) @@ -219,21 +222,19 @@ export class Guantr< resource: ResourceKey, action?: Meta extends GuantrMeta ? U[ResourceKey]['action'] : string ): R { - const relatedPermissions = this.relatedPermissionsFor( - action ?? 'read' as NonNullable, - resource - ).map(permission => ({ - ...permission, - condition: permission.condition - ? JSON.parse(JSON.stringify(permission.condition), (_, v) => { - if (isContextualOperand(v)) return getContextValue(this._context, v) ?? v - return v - }) as GuantrAnyPermission['condition'] - : null - })) + const relatedPermissions = this.relatedPermissionsFor(action ?? 'read' as NonNullable, resource) + .map(permission => ({ ...permission, condition: this.applyContextualOperands(permission.condition) })) return transformer(relatedPermissions) } + + private applyContextualOperands( + condition: GuantrAnyPermission['condition'] + ): GuantrAnyPermission['condition'] { + return condition + ? JSON.parse(JSON.stringify(condition), (_, value) => isContextualOperand(value) ? getContextValue(this._context, value) : value) + : null; + } } /** diff --git a/src/utils.ts b/src/utils.ts index c93417d..de2d8ad 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,30 @@ import { GuantrAnyConditionExpression, GuantrAnyPermission } from "./types" +/** + * Checks if the given path is a string and starts with either '$context.' or 'context.'. + * + * @param {unknown} path - The path to check. + * @return {boolean} - Returns true if the path is a string and starts with either '$context.' or 'context.', otherwise returns false. + */ +export const isContextualOperand = (path: unknown): path is string => typeof path === 'string' && (path.startsWith('$context.') || path.startsWith('context.')) + +/** + * Retrieves the value at the specified path from the given context object. + * + * @template T - The type of the context object. + * @template U - The type of the value to retrieve. + * @param {T} context - The context object to search in. + * @param {string} path - The dot-separated path to the value. + * @return {U | undefined} The value at the specified path, or undefined if not found. + */ +export const getContextValue = , U>(context: T, path: string): U | undefined => { + return (path.replace(path.startsWith('$') ? '$context.' : 'context.', '')) + .replaceAll('?.', '.') + .split('.') + // eslint-disable-next-line unicorn/no-array-reduce + .reduce((o, k) => (o || {})[k], (context ?? {}) as Record) as U | undefined +} + export const isValidConditionExpression = (maybeExpression: unknown): maybeExpression is GuantrAnyConditionExpression => { if (!Array.isArray(maybeExpression) || maybeExpression.length < 2 || typeof maybeExpression[0] !== 'string') return false return true @@ -9,24 +34,20 @@ export const isValidConditionExpression = (maybeExpression: unknown): maybeExpre * Checks if the given model matches the permission condition. * * @param {Model} model - The model to check against the permission condition. - * @param {GuantrAnyPermission & { condition: NonNullable }} permission - The permission object containing the condition to match. - * @param {Context} [context] - Optional context object for additional information. + * @param {GuantrAnyPermission & { condition: NonNullable }} condition - The condition to match. * @returns {boolean} Returns true if the model matches the permission condition, false otherwise. */ export const matchPermissionCondition = < Model extends Record, - Context extends Record | undefined = undefined, >( model: Model, condition: NonNullable, - context?: Context, ): boolean => { return Object.entries(condition).every(([key, expressionOrNestedCondition]) => { if (Array.isArray(expressionOrNestedCondition)) { return matchConditionExpression({ value: model[key], expression: expressionOrNestedCondition, - context, }) } else if (typeof expressionOrNestedCondition === 'object') { @@ -38,19 +59,10 @@ export const matchPermissionCondition = < } if ($expr) { - return ( - isValidConditionExpression($expr) ? matchConditionExpression({ - value: model[key], - expression: $expr, - context - }) : false - ) && matchPermissionCondition(nestedModel as Record, condition, context) - } - return matchPermissionCondition( - nestedModel as Record, - condition, - context - ) + return (isValidConditionExpression($expr) ? matchConditionExpression({ value: model[key], expression: $expr }) : false) + && matchPermissionCondition(nestedModel as Record, condition) + } + return matchPermissionCondition(nestedModel as Record, condition) } else { throw new TypeError(`Unexpected expression value type: ${typeof expressionOrNestedCondition}`) @@ -58,50 +70,24 @@ export const matchPermissionCondition = < }) } -/** - * Checks if the given path is a string and starts with either '$context.' or 'context.'. - * - * @param {unknown} path - The path to check. - * @return {boolean} - Returns true if the path is a string and starts with either '$context.' or 'context.', otherwise returns false. - */ -export const isContextualOperand = (path: unknown): path is string => typeof path === 'string' && (path.startsWith('$context.') || path.startsWith('context.')) -/** - * Retrieves the value at the specified path from the given context object. - * - * @template T - The type of the context object. - * @template U - The type of the value to retrieve. - * @param {T} context - The context object to search in. - * @param {string} path - The dot-separated path to the value. - * @return {U | undefined} The value at the specified path, or undefined if not found. - */ -export const getContextValue = , U>(context: T, path: string): U | undefined => { - return (path.replace(path.startsWith('$') ? '$context.' : 'context.', '')) - .replaceAll('?.', '.') - .split('.') - // eslint-disable-next-line unicorn/no-array-reduce - .reduce((o, k) => (o || {})[k], (context ?? {}) as Record) as U | undefined -} - /** * Evaluates a condition expression against a given value and context. * * @param {Object} data - The data object containing the value, expression, and optional context. * @param {unknown} data.value - The value to evaluate the condition against. * @param {NonNullable[keyof NonNullable]} data.expression - The condition expression to evaluate. - * @param {Record} [data.context] - The optional context object to use for evaluating the condition. * @return {boolean} The result of evaluating the condition expression against the value and context. * @throws {TypeError} If the model value type is unexpected or the operand type is invalid. */ export const matchConditionExpression = (data: { value: unknown expression: Extract[keyof NonNullable], Array> - context?: Record }): boolean => { - const { value, expression, context, } = data - - const [operator, maybeContextualOperand, options] = expression ?? [] - let operand = maybeContextualOperand - if (isContextualOperand(operand)) operand = getContextValue(context ?? {}, operand) + const { + value, + expression, + } = data + const [operator, operand, options] = expression ?? [] switch (operator) { case 'equals': { @@ -408,14 +394,12 @@ export const matchConditionExpression = (data: { return matchConditionExpression({ value: i[key], expression: expressionOrNestedCondition as any, - context, }) } else if (typeof expressionOrNestedCondition === 'object') { return matchPermissionCondition( i[key] as Record, expressionOrNestedCondition, - context ) } else { @@ -461,14 +445,12 @@ export const matchConditionExpression = (data: { return matchConditionExpression({ value: i[key], expression: expressionOrNestedCondition as any, - context, }) } else if (typeof expressionOrNestedCondition === 'object') { return matchPermissionCondition( i[key] as Record, expressionOrNestedCondition, - context ) } else { diff --git a/tests/operators/contains.test.ts b/tests/operators/contains.test.ts index 035cea6..dfdf42d 100644 --- a/tests/operators/contains.test.ts +++ b/tests/operators/contains.test.ts @@ -2,13 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - contains operator', () => { - const context = { - greeting: 'world', - keyword: 'test', - phrase: 'insensitive', - mixedCaseWord: 'CaseSensitive' - } - const testCases = [ // Valid cases where the value contains the operand { value: 'hello world', operand: 'world', expected: true }, @@ -24,12 +17,6 @@ describe('matchConditionExpression - contains operator', () => { { value: 'Case Insensitive Test', operand: 'case', options: { caseInsensitive: true }, expected: true }, { value: 'JavaScript is Fun', operand: 'javascript', options: { caseInsensitive: true }, expected: true }, - // Context usage - { value: 'hello world', operand: '$context.greeting', expected: true }, - { value: 'unit test coverage', operand: '$context.keyword', expected: true }, - { value: 'case insensitive check', operand: '$context.phrase', options: { caseInsensitive: true }, expected: true }, - { value: 'Checking for CaseSensitive word', operand: '$context.mixedCaseWord', options: { caseInsensitive: true }, expected: true }, - // Null and undefined values { value: null, operand: 'null', expected: false }, { value: undefined, operand: 'undefined', expected: false }, @@ -47,7 +34,7 @@ describe('matchConditionExpression - contains operator', () => { for (const [idx, { value, operand, options, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['contains', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -57,7 +44,7 @@ describe('matchConditionExpression - contains operator', () => { const value = 123 // Invalid type for 'contains' operator const operand = 'test' const expression = ['contains', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -65,6 +52,6 @@ describe('matchConditionExpression - contains operator', () => { const value = 'test' const operand = 123 // Operand must be a string const expression = ['contains', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) -}) \ No newline at end of file +}) diff --git a/tests/operators/endswith.test.ts b/tests/operators/endswith.test.ts index 367b705..d2c14c3 100644 --- a/tests/operators/endswith.test.ts +++ b/tests/operators/endswith.test.ts @@ -2,12 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - endsWith operator', () => { - const context = { - suffix1: 'world!', - suffix2: 'WORLD!', - suffix3: 'test', - } - const testCases = [ // Valid cases where the value ends with the operand { value: 'Hello, world!', operand: 'world!', expected: true }, @@ -21,11 +15,6 @@ describe('matchConditionExpression - endsWith operator', () => { { value: 'Hello, world!', operand: 'WORLD!', expected: true, options: { caseInsensitive: true } }, { value: 'Testing endsWith Operator', operand: 'operator', expected: true, options: { caseInsensitive: true } }, - // Context usage - { value: 'Hello, world!', operand: '$context.suffix1', expected: true }, - { value: 'Hello, world!', operand: '$context.suffix2', expected: true, options: { caseInsensitive: true } }, - { value: 'Hello, test', operand: '$context.suffix3', expected: true }, - // Null and undefined values { value: null, operand: 'suffix', expected: false }, { value: undefined, operand: 'suffix', expected: false }, @@ -38,7 +27,7 @@ describe('matchConditionExpression - endsWith operator', () => { for (const [idx, { value, operand, expected, options }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['endsWith', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -48,7 +37,7 @@ describe('matchConditionExpression - endsWith operator', () => { const value = { key: 'value' } // Invalid type for 'endsWith' operator const operand = 'value' const expression = ['endsWith', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -56,6 +45,6 @@ describe('matchConditionExpression - endsWith operator', () => { const value = 'string value' const operand = 123 // Invalid type for 'endsWith' operand const expression = ['endsWith', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/equals.test.ts b/tests/operators/equals.test.ts index 294c95e..e20e1eb 100644 --- a/tests/operators/equals.test.ts +++ b/tests/operators/equals.test.ts @@ -2,15 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - equals operator', () => { - const context = { - number: 42, - string: 'Hello, world!', - boolean: true, - null: null, - undefined: undefined, - caseSensitive: 'Test' - } - const testCases = [ // Null and undefined values { value: null, operand: null, expected: true }, @@ -35,20 +26,12 @@ describe('matchConditionExpression - equals operator', () => { { value: false, operand: false, expected: true }, { value: true, operand: false, expected: false }, { value: false, operand: true, expected: false }, - - // Context usage - { value: 42, operand: '$context.number', expected: true }, - { value: 'Hello, world!', operand: '$context.string', expected: true }, - { value: true, operand: '$context.boolean', expected: true }, - { value: null, operand: '$context.null', expected: true }, - { value: undefined, operand: '$context.undefined', expected: true }, - { value: 'Test', operand: '$context.caseSensitive', expected: true }, ] for (const [idx, { value, operand, options, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['equals', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -58,7 +41,7 @@ describe('matchConditionExpression - equals operator', () => { const value = { key: 'value' } const operand = 'test' const expression = ['equals', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -66,6 +49,6 @@ describe('matchConditionExpression - equals operator', () => { const value = 'test' const operand = { key: 'value' } const expression = ['equals', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) -}) \ No newline at end of file +}) diff --git a/tests/operators/every.test.ts b/tests/operators/every.test.ts index 655c128..36ec444 100644 --- a/tests/operators/every.test.ts +++ b/tests/operators/every.test.ts @@ -2,14 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - every operator', () => { - const context = { - threshold: 50, - status: 'active', - name: { - last: 'Doe' - } - } - const testCases = [ // Basic Checks { value: [{ id: 1, value: 60 }, { id: 2, value: 70 }, { id: 3, value: 80 }], operand: { value: ['gt', 50] }, expected: true }, // All items with value > 50 @@ -21,13 +13,8 @@ describe('matchConditionExpression - every operator', () => { { value: [{ id: 1, value: 60, status: 'active' }, { id: 2, value: 70, status: 'inactive' }], operand: { value: ['gt', 50], status: ['equals', 'active'] }, expected: false }, // Not all items with value > 50 and status === 'active' { value: [{ id: 1, name: 'alice', age: 25 }, { id: 2, name: 'bob', age: 30 }], operand: { name: ['equals', 'alice'], age: ['gte', 25] }, expected: false }, // Not all items with name === 'alice' and age >= 25 - // Contextual Checks - { value: [{ id: 1, value: 60 }, { id: 2, value: 70 }], operand: { value: ['gt', '$context.threshold'] }, expected: true }, // All items with value > context.threshold (50) - { value: [{ id: 1, value: 45 }, { id: 2, value: 50 }], operand: { value: ['gt', '$context.threshold'] }, expected: false }, // Not all items with value > context.threshold (50) - { value: [{ id: 1, status: 'active' }, { id: 2, status: 'active' }], operand: { status: ['equals', '$context.status'] }, expected: true }, // All items with status === context.status ('active') - // Nested - { value: [{ id: 1, name: { first: 'John', last: 'Doe' } }, { id: 2, name: { first: 'Alice', last: 'Doe' } }], operand: { name: { last: ['equals', '$context.name.last'] } }, expected: true }, // All items with name.last === context.name.last ('Doe') + { value: [{ id: 1, name: { first: 'John', last: 'Doe' } }, { id: 2, name: { first: 'Alice', last: 'Doe' } }], operand: { name: { last: ['equals', 'Doe'] } }, expected: true }, // All items with name.last === 'Doe' // Handling null and undefined { value: null, operand: { value: ['gt', 10] }, expected: false }, // null array @@ -40,7 +27,7 @@ describe('matchConditionExpression - every operator', () => { for (const [idx, { value, operand, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['every', operand] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -50,7 +37,7 @@ describe('matchConditionExpression - every operator', () => { const value = { key: 'value' } // Invalid type for 'every' operator const operand = { value: ['gt', 10] } const expression = ['every', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -58,6 +45,6 @@ describe('matchConditionExpression - every operator', () => { const value = [{ id: 1, value: 10 }] const operand = { key: 'value' } // Invalid type for 'every' operand const expression = ['every', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/gt.test.ts b/tests/operators/gt.test.ts index 9daccf3..80d0ebf 100644 --- a/tests/operators/gt.test.ts +++ b/tests/operators/gt.test.ts @@ -2,12 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - gt operator', () => { - const context = { - smallNumber: 5, - mediumNumber: 15, - largeNumber: 100 - } - const testCases = [ // Basic Comparisons { value: 20, operand: 10, expected: true }, // 20 > 10 @@ -15,11 +9,6 @@ describe('matchConditionExpression - gt operator', () => { { value: -5, operand: -10, expected: true }, // -5 > -10 { value: 0, operand: 0, expected: false }, // 0 == 0 - // Contextual Comparisons - { value: 20, operand: '$context.smallNumber', expected: true }, // 20 > 5 (from context) - { value: 10, operand: '$context.mediumNumber', expected: false }, // 10 < 15 (from context) - { value: 50, operand: '$context.largeNumber', expected: false }, // 50 < 100 (from context) - // Handling null and undefined { value: null, operand: 10, expected: false }, // null is not greater than 10 { value: undefined, operand: 10, expected: false }, // undefined is not greater than 10 @@ -28,7 +17,7 @@ describe('matchConditionExpression - gt operator', () => { for (const [idx, { value, operand, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['gt', operand] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -38,7 +27,7 @@ describe('matchConditionExpression - gt operator', () => { const value = { key: 'value' } // Invalid type for 'gt' operator const operand = 10 const expression = ['gt', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -46,6 +35,6 @@ describe('matchConditionExpression - gt operator', () => { const value = 10 const operand = { key: 'value' } // Invalid type for 'gt' operand const expression = ['gt', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/gte.test.ts b/tests/operators/gte.test.ts index 03359ea..cab93ab 100644 --- a/tests/operators/gte.test.ts +++ b/tests/operators/gte.test.ts @@ -2,12 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - gte operator', () => { - const context = { - smallNumber: 5, - mediumNumber: 15, - largeNumber: 100 - } - const testCases = [ // Basic Comparisons { value: 20, operand: 10, expected: true }, // 20 >= 10 @@ -15,11 +9,6 @@ describe('matchConditionExpression - gte operator', () => { { value: -5, operand: -10, expected: true }, // -5 > -10 { value: 0, operand: 10, expected: false }, // 0 < 10 - // Contextual Comparisons - { value: 20, operand: '$context.smallNumber', expected: true }, // 20 >= 5 (from context) - { value: 10, operand: '$context.mediumNumber', expected: false }, // 10 < 15 (from context) - { value: 100, operand: '$context.largeNumber', expected: true }, // 100 == 100 (from context) - // Handling null and undefined { value: null, operand: 10, expected: false }, // null is not greater than or equal to 10 { value: undefined, operand: 10, expected: false }, // undefined is not greater than or equal to 10 @@ -28,7 +17,7 @@ describe('matchConditionExpression - gte operator', () => { for (const [idx, { value, operand, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['gte', operand] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -38,7 +27,7 @@ describe('matchConditionExpression - gte operator', () => { const value = { key: 'value' } // Invalid type for 'gte' operator const operand = 10 const expression = ['gte', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -46,6 +35,6 @@ describe('matchConditionExpression - gte operator', () => { const value = 10 const operand = { key: 'value' } // Invalid type for 'gte' operand const expression = ['gte', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/has-every.test.ts b/tests/operators/has-every.test.ts index 172f534..6a9a3f9 100644 --- a/tests/operators/has-every.test.ts +++ b/tests/operators/has-every.test.ts @@ -2,14 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - hasEvery operator', () => { - const context = { - stringElements: ['foo', 'bar'], - numberElements: [1, 42, 100], - mixedArray: ['one', 2, 'three', 4], - singleString: 'single', - singleNumber: 10 - } - const testCases = [ // Basic Array Checks { value: ['a', 'b', 'c'], operand: ['a', 'b'], expected: true }, // 'a' and 'b' in ['a', 'b', 'c'] @@ -17,11 +9,6 @@ describe('matchConditionExpression - hasEvery operator', () => { { value: ['x', 'y', 'z'], operand: ['y', 'a'], expected: false }, // 'y' and 'a' not both in ['x', 'y', 'z'] { value: ['a', 'b'], operand: ['a', 'b', 'c'], expected: false }, // ['a', 'b', 'c'] not all in ['a', 'b'] - // Contextual Checks - { value: ['foo', 'bar', 'baz'], operand: '$context.stringElements', expected: true }, // 'foo' and 'bar' (from context) in ['foo', 'bar', 'baz'] - { value: [1, 2, 42], operand: '$context.numberElements', expected: false }, // 1, 42, and 100 (from context) not all in [1, 2, 42] - { value: ['one', 2, 'three', 4], operand: '$context.mixedArray', expected: true }, // 'one', 2, 'three', and 4 (from context) in ['one', 2, 'three', 4] - // Handling null and undefined { value: null, operand: ['element'], expected: false }, // null array { value: undefined, operand: ['element'], expected: false }, // undefined array @@ -40,7 +27,7 @@ describe('matchConditionExpression - hasEvery operator', () => { for (const [idx, { value, operand, expected, options }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['hasEvery', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -50,7 +37,7 @@ describe('matchConditionExpression - hasEvery operator', () => { const value = { key: 'value' } // Invalid type for 'hasEvery' operator const operand = ['element'] const expression = ['hasEvery', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -58,6 +45,6 @@ describe('matchConditionExpression - hasEvery operator', () => { const value = ['a', 'b', 'c'] const operand = { key: 'value' } // Invalid type for 'hasEvery' operand const expression = ['hasEvery', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/has-some.test.ts b/tests/operators/has-some.test.ts index f3b03b1..55e0264 100644 --- a/tests/operators/has-some.test.ts +++ b/tests/operators/has-some.test.ts @@ -2,14 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - hasSome operator', () => { - const context = { - stringElements: ['foo', 'bar'], - numberElements: [1, 42, 100], - mixedArray: ['one', 2, 'three', 4], - singleString: 'single', - singleNumber: 10 - } - const testCases = [ // Basic Array Checks { value: ['a', 'b', 'c'], operand: ['a', 'd'], expected: true }, // 'a' or 'd' in ['a', 'b', 'c'] @@ -17,12 +9,6 @@ describe('matchConditionExpression - hasSome operator', () => { { value: ['x', 'y', 'z'], operand: ['a', 'b'], expected: false }, // 'a' or 'b' not in ['x', 'y', 'z'] { value: [], operand: ['a', 'b'], expected: false }, // empty array - // Contextual Checks - { value: ['foo', 'baz'], operand: '$context.stringElements', expected: true }, // 'foo' or 'bar' (from context) in ['foo', 'baz'] - { value: [10, 20, 30], operand: '$context.numberElements', expected: false }, // 1 or 42 or 100 (from context) in [10, 20, 30] - { value: [50, 60, 70], operand: '$context.numberElements', expected: false }, // 1 or 42 or 100 (from context) not in [50, 60, 70] - { value: ['one', 2, 'three'], operand: '$context.mixedArray', expected: true }, // 'one', 2, 'three', 4 (from context) in ['one', 2, 'three'] - // Handling null and undefined { value: null, operand: ['element'], expected: false }, // null array { value: undefined, operand: ['element'], expected: false }, // undefined array @@ -41,7 +27,7 @@ describe('matchConditionExpression - hasSome operator', () => { for (const [idx, { value, operand, expected, options }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['hasSome', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -51,7 +37,7 @@ describe('matchConditionExpression - hasSome operator', () => { const value = { key: 'value' } // Invalid type for 'hasSome' operator const operand = ['element'] const expression = ['hasSome', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -59,6 +45,6 @@ describe('matchConditionExpression - hasSome operator', () => { const value = ['a', 'b', 'c'] const operand = { key: 'value' } // Invalid type for 'hasSome' operand const expression = ['hasSome', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/has.test.ts b/tests/operators/has.test.ts index 18db185..a063c59 100644 --- a/tests/operators/has.test.ts +++ b/tests/operators/has.test.ts @@ -2,11 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - has operator', () => { - const context = { - stringElement: 'element', - numberElement: 42, - } - const testCases = [ // Basic Array Checks { value: ['a', 'b', 'c'], operand: 'a', expected: true }, // 'a' in ['a', 'b', 'c'] @@ -14,12 +9,6 @@ describe('matchConditionExpression - has operator', () => { { value: ['x', 'y', 'z'], operand: 'a', expected: false }, // 'a' not in ['x', 'y', 'z'] { value: [], operand: 'element', expected: false }, // 'element' not in [] - // Contextual Checks - { value: ['element', 'other'], operand: '$context.stringElement', expected: true }, // 'element' (from context) in ['element', 'other'] - { value: [10, 42, 30], operand: '$context.numberElement', expected: true }, // 42 (from context) in [10, 42, 30] - { value: [1, 2, 3], operand: '$context.numberElement', expected: false }, // 42 (from context) not in [1, 2, 3] - { value: ['one', 2, 'three', 4], operand: 1, expected: false }, // 'three' (from context) in ['one', 2, 'three', 4] - // Handling null and undefined { value: null, operand: 'element', expected: false }, // null array { value: undefined, operand: 'element', expected: false }, // undefined array @@ -37,7 +26,7 @@ describe('matchConditionExpression - has operator', () => { for (const [idx, { value, operand, expected, options }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['has', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -47,7 +36,7 @@ describe('matchConditionExpression - has operator', () => { const value = { key: 'value' } // Invalid type for 'has' operator const operand = 'element' const expression = ['has', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -55,6 +44,6 @@ describe('matchConditionExpression - has operator', () => { const value = ['a', 'b', 'c'] const operand = { key: 'value' } // Invalid type for 'has' operand const expression = ['has', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/in.test.ts b/tests/operators/in.test.ts index 93d3585..db66cc9 100644 --- a/tests/operators/in.test.ts +++ b/tests/operators/in.test.ts @@ -2,13 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - in operator', () => { - const context = { - listOfNumbers: [1, 2, 3, 4, 5], - listOfStrings: ['apple', 'banana', 'cherry'], - emptyList: [], - mixedCaseList: ['Test', 'Case', 'Insensitive'] - } - const testCases = [ // Valid cases where the value is in the operand array { value: 3, operand: [1, 2, 3, 4, 5], expected: true }, @@ -24,13 +17,6 @@ describe('matchConditionExpression - in operator', () => { { value: 'test', operand: ['test', 'TEST', 'TeSt'], options: { caseInsensitive: true }, expected: true }, { value: 'case', operand: ['CaSe', 'CASE', 'case'], options: { caseInsensitive: true }, expected: true }, - // Context usage - { value: 3, operand: '$context.listOfNumbers', expected: true }, - { value: 'banana', operand: '$context.listOfStrings', expected: true }, - { value: 'case', operand: '$context.mixedCaseList', options: { caseInsensitive: true }, expected: true }, - { value: 'pear', operand: '$context.listOfStrings', expected: false }, - { value: 'empty', operand: '$context.emptyList', expected: false }, - // Null and undefined values { value: null, operand: ['null', 0], expected: false }, { value: undefined, operand: ['undefined', 0], expected: false }, @@ -47,7 +33,7 @@ describe('matchConditionExpression - in operator', () => { for (const [idx, { value, operand, options, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['in', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -57,7 +43,7 @@ describe('matchConditionExpression - in operator', () => { const value = { key: 'value' } // Invalid type for 'in' operator const operand = [1, 2, 3] const expression = ['in', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -65,6 +51,6 @@ describe('matchConditionExpression - in operator', () => { const value = 'test' const operand = 'not an array' // Operand must be an array const expression = ['in', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/some.test.ts b/tests/operators/some.test.ts index eb31738..8685a52 100644 --- a/tests/operators/some.test.ts +++ b/tests/operators/some.test.ts @@ -2,14 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - some operator', () => { - const context = { - threshold: 50, - status: 'active', - name: { - last: 'Doe' - } - } - const testCases = [ // Basic Checks { value: [{ id: 1, value: 10 }, { id: 2, value: 20 }, { id: 3, value: 30 }], operand: { value: ['gt', 15] }, expected: true }, // One item with value > 15 @@ -21,15 +13,10 @@ describe('matchConditionExpression - some operator', () => { { value: [{ id: 1, value: 10, status: 'active' }, { id: 2, value: 30, status: 'inactive' }], operand: { value: ['gt', 15], status: ['equals', 'inactive'] }, expected: true }, // One item with value > 15 and status === 'inactive' { value: [{ id: 1, name: 'alice', age: 25 }, { id: 2, name: 'bob', age: 30 }], operand: { name: ['equals', 'alice'], age: ['gte', 25] }, expected: true }, // One item with name === 'alice' and age >= 25 - // Contextual Checks - { value: [{ id: 1, value: 45 }, { id: 2, value: 55 }], operand: { value: ['gt', '$context.threshold'] }, expected: true }, // One item with value > context.threshold (50) - { value: [{ id: 1, value: 45 }, { id: 2, value: 50 }], operand: { value: ['gt', '$context.threshold'] }, expected: false }, // No item with value > context.threshold (50) - { value: [{ id: 1, status: 'active' }, { id: 2, status: 'inactive' }], operand: { status: ['equals', '$context.status'] }, expected: true }, // One item with status === context.status ('active') - // Nested { value: [{ id: 1, name: { first: 'John', last: 'Doe' } }, { id: 2, name: { first: 'Alice', last: 'Smith' } }], operand: { name: { first: ['equals', 'Alice'] } }, expected: true }, - { value: [{ id: 1, name: { first: 'John', last: 'Doe' } }, { id: 2, name: { first: 'Alice', last: 'Smith' } }], operand: { name: { first: ['equals', '$context.name.last'] } }, expected: false }, - { value: [{ id: 1, name: { first: 'John', last: 'Doe' } }, { id: 2, name: { first: 'Alice', last: 'Smith' } }], operand: { name: { last: ['equals', '$context.name.last'] } }, expected: true }, + { value: [{ id: 1, name: { first: 'John', last: 'Doe' } }, { id: 2, name: { first: 'Alice', last: 'Smith' } }], operand: { name: { first: ['equals', 'Doe'] } }, expected: false }, + { value: [{ id: 1, name: { first: 'John', last: 'Doe' } }, { id: 2, name: { first: 'Alice', last: 'Smith' } }], operand: { name: { last: ['equals', 'Doe'] } }, expected: true }, // Handling null and undefined { value: null, operand: { value: ['gt', 10] }, expected: false }, // null array @@ -43,7 +30,7 @@ describe('matchConditionExpression - some operator', () => { for (const [idx, { value, operand, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['some', operand] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -53,7 +40,7 @@ describe('matchConditionExpression - some operator', () => { const value = { key: 'value' } // Invalid type for 'some' operator const operand = { value: ['gt', 10] } const expression = ['some', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -61,6 +48,6 @@ describe('matchConditionExpression - some operator', () => { const value = [{ id: 1, value: 10 }] const operand = { key: 'value' } // Invalid type for 'some' operand const expression = ['some', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) }) diff --git a/tests/operators/startswith.test.ts b/tests/operators/startswith.test.ts index 2051c02..297f12f 100644 --- a/tests/operators/startswith.test.ts +++ b/tests/operators/startswith.test.ts @@ -2,13 +2,6 @@ import { describe, expect, it } from "vitest" import { matchConditionExpression } from "../../src/utils" describe('matchConditionExpression - startsWith operator', () => { - const context = { - user: 'AdminUser', - prefix: 'hello', - subString: 'Java', - caseInsensitiveWord: 'Case' - } - const testCases = [ // Valid cases where the value starts with the operand { value: 'hello world', operand: 'hello', expected: true }, @@ -24,12 +17,6 @@ describe('matchConditionExpression - startsWith operator', () => { { value: 'Case Insensitive Test', operand: 'case', options: { caseInsensitive: true }, expected: true }, { value: 'Vitest Is Great', operand: 'vitest', options: { caseInsensitive: true }, expected: true }, - // Context usage - { value: 'hello world', operand: '$context.prefix', expected: true }, - { value: 'JavaScript rules', operand: '$context.subString', expected: true }, - { value: 'AdminUser rules', operand: '$context.user', expected: true }, - { value: 'Case insensitive context', operand: '$context.caseInsensitiveWord', options: { caseInsensitive: true }, expected: true }, - // Null and undefined values { value: null, operand: 'null', expected: false }, { value: undefined, operand: 'undefined', expected: false }, @@ -47,7 +34,7 @@ describe('matchConditionExpression - startsWith operator', () => { for (const [idx, { value, operand, options, expected }] of testCases.entries()) { it(`should return ${expected} for case #${idx + 1}`, () => { const expression = ['startsWith', operand, options] as any - const result = matchConditionExpression({ value, expression, context }) + const result = matchConditionExpression({ value, expression }) expect(result).toBe(expected) }) } @@ -57,7 +44,7 @@ describe('matchConditionExpression - startsWith operator', () => { const value = 123 // Invalid type for 'startsWith' operator const operand = 'test' const expression = ['startsWith', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) // Edge case: invalid operand type @@ -65,6 +52,6 @@ describe('matchConditionExpression - startsWith operator', () => { const value = 'test' const operand = 123 // Operand must be a string const expression = ['startsWith', operand] as any - expect(() => matchConditionExpression({ value, expression, context })).toThrow(TypeError) + expect(() => matchConditionExpression({ value, expression })).toThrow(TypeError) }) -}) \ No newline at end of file +})