diff --git a/packages/taco/package.json b/packages/taco/package.json index fcf61b29..0b3f0f8a 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -39,6 +39,7 @@ "typedoc": "typedoc" }, "dependencies": { + "@astronautlabs/jsonpath": "^1.1.2", "@nucypher/nucypher-core": "*", "@nucypher/shared": "workspace:*", "@nucypher/taco-auth": "workspace:*", diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index 0a55b20b..22032cb3 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,18 +1,31 @@ +import { JSONPath } from '@astronautlabs/jsonpath'; import { z } from 'zod'; import { Condition } from '../condition'; -import { - OmitConditionType, - returnValueTestSchema, -} from '../shared'; +import { OmitConditionType, returnValueTestSchema } from '../shared'; export const JsonApiConditionType = 'json-api'; +const validateJSONPath = (jsonPath: string): boolean => { + try { + JSONPath.parse(jsonPath); + return true; + } catch (error) { + return false; + } +}; + +export const jsonPathSchema = z + .string() + .refine((val) => validateJSONPath(val), { + message: 'Invalid JSONPath expression', + }); + export const JsonApiConditionSchema = z.object({ conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType), endpoint: z.string().url(), parameters: z.record(z.string(), z.unknown()).optional(), - query: z.string().optional(), + query: jsonPathSchema.optional(), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); diff --git a/packages/taco/test/conditions/base/json-api.test.ts b/packages/taco/test/conditions/base/json-api.test.ts new file mode 100644 index 00000000..09c622e6 --- /dev/null +++ b/packages/taco/test/conditions/base/json-api.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import { jsonPathSchema } from '../../../src/conditions/base/json-api'; + +describe('JSONPath Validation', () => { + it('Invalid JSONPath: Incomplete filter expression', () => { + const invalidPath = '$.store.book[?(@.price < ]'; + const result = jsonPathSchema.safeParse(invalidPath); + expect(result.success).toBe(false); + expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); + }); + + it('Invalid JSONPath: Incorrect use of brackets', () => { + const invalidPath = '$[store][book]'; + const result = jsonPathSchema.safeParse(invalidPath); + expect(result.success).toBe(false); + expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); + }); + + it('Invalid JSONPath: Unclosed wildcard asterisk', () => { + const invalidPath = '$.store.book[*'; + const result = jsonPathSchema.safeParse(invalidPath); + expect(result.success).toBe(false); + expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression'); + }); + + it('Valid JSONPath expression', () => { + const validPath = '$.store.book[?(@.price < 10)]'; + const result = jsonPathSchema.safeParse(validPath); + expect(result.success).toBe(true); + }); + + it('Valid JSONPath with correct quotes', () => { + const validPath = "$.store['book[?(@.price < ]']"; + const result = jsonPathSchema.safeParse(validPath); + expect(result.success).toBe(true); + }); + + it('Valid JSONPath with correct wildcard', () => { + const validPath = '$.store.book[*]'; + const result = jsonPathSchema.safeParse(validPath); + expect(result.success).toBe(true); + }); +}); diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 9c26a7f7..0166485a 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -25,7 +25,10 @@ describe('JsonApiCondition', () => { endpoint: 'not-a-url', }; - const result = JsonApiCondition.validate(JsonApiConditionSchema, badJsonApiObj); + const result = JsonApiCondition.validate( + JsonApiConditionSchema, + badJsonApiObj, + ); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -35,26 +38,26 @@ describe('JsonApiCondition', () => { }, }); }); - + describe('parameters', () => { it('accepts conditions without query path', () => { - const { query, ...noQueryObj} = testJsonApiConditionObj; + const { query, ...noQueryObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( JsonApiConditionSchema, - noQueryObj + noQueryObj, ); - + expect(result.error).toBeUndefined(); expect(result.data).toEqual(noQueryObj); }); it('accepts conditions without parameters', () => { - const { query, ...noParamsObj} = testJsonApiConditionObj; + const { query, ...noParamsObj } = testJsonApiConditionObj; const result = JsonApiCondition.validate( JsonApiConditionSchema, - noParamsObj + noParamsObj, ); - + expect(result.error).toBeUndefined(); expect(result.data).toEqual(noParamsObj); }); diff --git a/packages/taco/test/conditions/condition-expr.test.ts b/packages/taco/test/conditions/condition-expr.test.ts index 4defa46c..66bf4829 100644 --- a/packages/taco/test/conditions/condition-expr.test.ts +++ b/packages/taco/test/conditions/condition-expr.test.ts @@ -406,17 +406,20 @@ describe('condition set', () => { it('json api condition serialization', () => { const conditionExpr = new ConditionExpression(jsonApiCondition); - + const conditionExprJson = conditionExpr.toJson(); expect(conditionExprJson).toBeDefined(); expect(conditionExprJson).toContain('endpoint'); - expect(conditionExprJson).toContain('https://_this_would_totally_fail.com'); + expect(conditionExprJson).toContain( + 'https://_this_would_totally_fail.com', + ); expect(conditionExprJson).toContain('parameters'); expect(conditionExprJson).toContain('query'); expect(conditionExprJson).toContain('$.ethereum.usd'); expect(conditionExprJson).toContain('returnValueTest'); - - const conditionExprFromJson = ConditionExpression.fromJSON(conditionExprJson); + + const conditionExprFromJson = + ConditionExpression.fromJSON(conditionExprJson); expect(conditionExprFromJson).toBeDefined(); expect(conditionExprFromJson.condition).toBeInstanceOf(JsonApiCondition); }); diff --git a/packages/taco/test/test-utils.ts b/packages/taco/test/test-utils.ts index 78db4f91..b82eb570 100644 --- a/packages/taco/test/test-utils.ts +++ b/packages/taco/test/test-utils.ts @@ -39,10 +39,7 @@ import { ContractConditionType, FunctionAbiProps, } from '../src/conditions/base/contract'; -import { - JsonApiConditionProps, - JsonApiConditionType -} from '../src/conditions/base/json-api'; +import { JsonApiConditionType } from '../src/conditions/base/json-api'; import { RpcConditionProps, RpcConditionType, @@ -230,8 +227,8 @@ export const testJsonApiConditionObj = { conditionType: JsonApiConditionType, endpoint: 'https://_this_would_totally_fail.com', parameters: { - 'ids': 'ethereum', - 'vs_currencies': 'usd', + ids: 'ethereum', + vs_currencies: 'usd', }, query: '$.ethereum.usd', returnValueTest: testReturnValueTest, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4937ad55..91308e3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -540,6 +540,9 @@ importers: packages/taco: dependencies: + '@astronautlabs/jsonpath': + specifier: ^1.1.2 + version: 1.1.2 '@nucypher/nucypher-core': specifier: ^0.14.5 version: 0.14.5 @@ -634,6 +637,9 @@ packages: resolution: {integrity: sha512-NqJDx39sHN0o7BpxpQzishoZjGBzvXqSVxO+bRm6OPP/Oe+Kb51R5x8dWvW9i3amO3QNdocg/p4jFRCwzqy2Gg==} engines: {node: '>=11.0.0'} + '@astronautlabs/jsonpath@1.1.2': + resolution: {integrity: sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A==} + '@babel/code-frame@7.24.6': resolution: {integrity: sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==} engines: {node: '>=6.9.0'} @@ -9044,6 +9050,10 @@ snapshots: transitivePeerDependencies: - debug + '@astronautlabs/jsonpath@1.1.2': + dependencies: + static-eval: 2.0.2 + '@babel/code-frame@7.24.6': dependencies: '@babel/highlight': 7.24.6 @@ -13986,8 +13996,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.4.5) eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.56.0) eslint-plugin-react: 7.34.1(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.56.0) @@ -14058,13 +14068,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0))(eslint@8.56.0): dependencies: debug: 4.3.5 enhanced-resolve: 5.17.1 eslint: 8.56.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -14096,24 +14106,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0))(eslint@8.56.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.4.5) eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.56.0 - eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0))(eslint@8.56.0) transitivePeerDependencies: - supports-color @@ -14195,7 +14195,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -14205,7 +14205,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -14216,7 +14216,7 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.4.5) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack