@digital-science-dsl/dimensions-analytics-mcp 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +152 -0
- package/bin/dimensions-analytics-mcp-http.js +2 -0
- package/bin/dimensions-analytics-mcp.js +2 -0
- package/dist/client/auth/config-loaders.d.ts +22 -0
- package/dist/client/auth/config-loaders.d.ts.map +1 -0
- package/dist/client/auth/config-loaders.js +68 -0
- package/dist/client/auth/config-loaders.js.map +1 -0
- package/dist/client/auth/decorators/index.d.ts +7 -0
- package/dist/client/auth/decorators/index.d.ts.map +1 -0
- package/dist/client/auth/decorators/index.js +6 -0
- package/dist/client/auth/decorators/index.js.map +1 -0
- package/dist/client/auth/decorators/with-caching.d.ts +81 -0
- package/dist/client/auth/decorators/with-caching.d.ts.map +1 -0
- package/dist/client/auth/decorators/with-caching.js +168 -0
- package/dist/client/auth/decorators/with-caching.js.map +1 -0
- package/dist/client/auth/factory.d.ts +13 -0
- package/dist/client/auth/factory.d.ts.map +1 -0
- package/dist/client/auth/factory.js +29 -0
- package/dist/client/auth/factory.js.map +1 -0
- package/dist/client/auth/index.d.ts +7 -0
- package/dist/client/auth/index.d.ts.map +1 -0
- package/dist/client/auth/index.js +6 -0
- package/dist/client/auth/index.js.map +1 -0
- package/dist/client/auth/jwt-utils.d.ts +17 -0
- package/dist/client/auth/jwt-utils.d.ts.map +1 -0
- package/dist/client/auth/jwt-utils.js +33 -0
- package/dist/client/auth/jwt-utils.js.map +1 -0
- package/dist/client/auth/node.d.ts +16 -0
- package/dist/client/auth/node.d.ts.map +1 -0
- package/dist/client/auth/node.js +15 -0
- package/dist/client/auth/node.js.map +1 -0
- package/dist/client/auth/providers/index.d.ts +2 -0
- package/dist/client/auth/providers/index.d.ts.map +1 -0
- package/dist/client/auth/providers/index.js +2 -0
- package/dist/client/auth/providers/index.js.map +1 -0
- package/dist/client/auth/providers/jwt-auth-provider.d.ts +70 -0
- package/dist/client/auth/providers/jwt-auth-provider.d.ts.map +1 -0
- package/dist/client/auth/providers/jwt-auth-provider.js +134 -0
- package/dist/client/auth/providers/jwt-auth-provider.js.map +1 -0
- package/dist/client/auth/schemas.d.ts +19 -0
- package/dist/client/auth/schemas.d.ts.map +1 -0
- package/dist/client/auth/schemas.js +23 -0
- package/dist/client/auth/schemas.js.map +1 -0
- package/dist/client/auth/types.d.ts +25 -0
- package/dist/client/auth/types.d.ts.map +1 -0
- package/dist/client/auth/types.js +9 -0
- package/dist/client/auth/types.js.map +1 -0
- package/dist/client/command.d.ts +44 -0
- package/dist/client/command.d.ts.map +1 -0
- package/dist/client/command.js +23 -0
- package/dist/client/command.js.map +1 -0
- package/dist/client/config/index.d.ts +11 -0
- package/dist/client/config/index.d.ts.map +1 -0
- package/dist/client/config/index.js +8 -0
- package/dist/client/config/index.js.map +1 -0
- package/dist/client/config/loader.d.ts +27 -0
- package/dist/client/config/loader.d.ts.map +1 -0
- package/dist/client/config/loader.js +128 -0
- package/dist/client/config/loader.js.map +1 -0
- package/dist/client/config/node.d.ts +18 -0
- package/dist/client/config/node.d.ts.map +1 -0
- package/dist/client/config/node.js +17 -0
- package/dist/client/config/node.js.map +1 -0
- package/dist/client/config/schemas.d.ts +23 -0
- package/dist/client/config/schemas.d.ts.map +1 -0
- package/dist/client/config/schemas.js +24 -0
- package/dist/client/config/schemas.js.map +1 -0
- package/dist/client/config/types.d.ts +37 -0
- package/dist/client/config/types.d.ts.map +1 -0
- package/dist/client/config/types.js +6 -0
- package/dist/client/config/types.js.map +1 -0
- package/dist/client/config/unified-loader.d.ts +41 -0
- package/dist/client/config/unified-loader.d.ts.map +1 -0
- package/dist/client/config/unified-loader.js +86 -0
- package/dist/client/config/unified-loader.js.map +1 -0
- package/dist/client/config.d.ts +43 -0
- package/dist/client/config.d.ts.map +1 -0
- package/dist/client/config.js +56 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/deployment-config.d.ts +36 -0
- package/dist/client/deployment-config.d.ts.map +1 -0
- package/dist/client/deployment-config.js +79 -0
- package/dist/client/deployment-config.js.map +1 -0
- package/dist/client/errors.d.ts +180 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +253 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/http-client.d.ts +215 -0
- package/dist/client/http-client.d.ts.map +1 -0
- package/dist/client/http-client.js +419 -0
- package/dist/client/http-client.js.map +1 -0
- package/dist/client/index.d.ts +28 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/internal-dsl-client.d.ts +30 -0
- package/dist/client/internal-dsl-client.d.ts.map +1 -0
- package/dist/client/internal-dsl-client.js +101 -0
- package/dist/client/internal-dsl-client.js.map +1 -0
- package/dist/client/rate-limiter.d.ts +79 -0
- package/dist/client/rate-limiter.d.ts.map +1 -0
- package/dist/client/rate-limiter.js +118 -0
- package/dist/client/rate-limiter.js.map +1 -0
- package/dist/client/resolve-api-key-user.d.ts +24 -0
- package/dist/client/resolve-api-key-user.d.ts.map +1 -0
- package/dist/client/resolve-api-key-user.js +80 -0
- package/dist/client/resolve-api-key-user.js.map +1 -0
- package/dist/client/types.d.ts +44 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +10 -0
- package/dist/client/types.js.map +1 -0
- package/dist/dsl/client.d.ts +330 -0
- package/dist/dsl/client.d.ts.map +1 -0
- package/dist/dsl/client.js +433 -0
- package/dist/dsl/client.js.map +1 -0
- package/dist/dsl/commands/apply-filters.d.ts +15 -0
- package/dist/dsl/commands/apply-filters.d.ts.map +1 -0
- package/dist/dsl/commands/apply-filters.js +29 -0
- package/dist/dsl/commands/apply-filters.js.map +1 -0
- package/dist/dsl/commands/facet/index.d.ts +6 -0
- package/dist/dsl/commands/facet/index.d.ts.map +1 -0
- package/dist/dsl/commands/facet/index.js +6 -0
- package/dist/dsl/commands/facet/index.js.map +1 -0
- package/dist/dsl/commands/facet/schemas.d.ts +71 -0
- package/dist/dsl/commands/facet/schemas.d.ts.map +1 -0
- package/dist/dsl/commands/facet/schemas.js +84 -0
- package/dist/dsl/commands/facet/schemas.js.map +1 -0
- package/dist/dsl/commands/functions/ClassifyCommand.d.ts +96 -0
- package/dist/dsl/commands/functions/ClassifyCommand.d.ts.map +1 -0
- package/dist/dsl/commands/functions/ClassifyCommand.js +95 -0
- package/dist/dsl/commands/functions/ClassifyCommand.js.map +1 -0
- package/dist/dsl/commands/functions/ExtractAffiliationsCommand.d.ts +86 -0
- package/dist/dsl/commands/functions/ExtractAffiliationsCommand.d.ts.map +1 -0
- package/dist/dsl/commands/functions/ExtractAffiliationsCommand.js +131 -0
- package/dist/dsl/commands/functions/ExtractAffiliationsCommand.js.map +1 -0
- package/dist/dsl/commands/functions/ExtractConceptsCommand.d.ts +73 -0
- package/dist/dsl/commands/functions/ExtractConceptsCommand.d.ts.map +1 -0
- package/dist/dsl/commands/functions/ExtractConceptsCommand.js +110 -0
- package/dist/dsl/commands/functions/ExtractConceptsCommand.js.map +1 -0
- package/dist/dsl/commands/functions/ExtractGrantsCommand.d.ts +66 -0
- package/dist/dsl/commands/functions/ExtractGrantsCommand.d.ts.map +1 -0
- package/dist/dsl/commands/functions/ExtractGrantsCommand.js +87 -0
- package/dist/dsl/commands/functions/ExtractGrantsCommand.js.map +1 -0
- package/dist/dsl/commands/functions/index.d.ts +10 -0
- package/dist/dsl/commands/functions/index.d.ts.map +1 -0
- package/dist/dsl/commands/functions/index.js +10 -0
- package/dist/dsl/commands/functions/index.js.map +1 -0
- package/dist/dsl/commands/index.d.ts +8 -0
- package/dist/dsl/commands/index.d.ts.map +1 -0
- package/dist/dsl/commands/index.js +7 -0
- package/dist/dsl/commands/index.js.map +1 -0
- package/dist/dsl/commands/validate-input.d.ts +18 -0
- package/dist/dsl/commands/validate-input.d.ts.map +1 -0
- package/dist/dsl/commands/validate-input.js +27 -0
- package/dist/dsl/commands/validate-input.js.map +1 -0
- package/dist/dsl/create-client.d.ts +32 -0
- package/dist/dsl/create-client.d.ts.map +1 -0
- package/dist/dsl/create-client.js +58 -0
- package/dist/dsl/create-client.js.map +1 -0
- package/dist/dsl/fluent-query-builder.d.ts +439 -0
- package/dist/dsl/fluent-query-builder.d.ts.map +1 -0
- package/dist/dsl/fluent-query-builder.js +534 -0
- package/dist/dsl/fluent-query-builder.js.map +1 -0
- package/dist/dsl/index.d.ts +32 -0
- package/dist/dsl/index.d.ts.map +1 -0
- package/dist/dsl/index.js +31 -0
- package/dist/dsl/index.js.map +1 -0
- package/dist/dsl/pagination.d.ts +82 -0
- package/dist/dsl/pagination.d.ts.map +1 -0
- package/dist/dsl/pagination.js +135 -0
- package/dist/dsl/pagination.js.map +1 -0
- package/dist/dsl/query-builder.d.ts +456 -0
- package/dist/dsl/query-builder.d.ts.map +1 -0
- package/dist/dsl/query-builder.js +994 -0
- package/dist/dsl/query-builder.js.map +1 -0
- package/dist/dsl/response-parser.d.ts +111 -0
- package/dist/dsl/response-parser.d.ts.map +1 -0
- package/dist/dsl/response-parser.js +210 -0
- package/dist/dsl/response-parser.js.map +1 -0
- package/dist/dsl/schema/cache.d.ts +52 -0
- package/dist/dsl/schema/cache.d.ts.map +1 -0
- package/dist/dsl/schema/cache.js +97 -0
- package/dist/dsl/schema/cache.js.map +1 -0
- package/dist/dsl/schema/extract.d.ts +18 -0
- package/dist/dsl/schema/extract.d.ts.map +1 -0
- package/dist/dsl/schema/extract.js +41 -0
- package/dist/dsl/schema/extract.js.map +1 -0
- package/dist/dsl/schema/index.d.ts +13 -0
- package/dist/dsl/schema/index.d.ts.map +1 -0
- package/dist/dsl/schema/index.js +12 -0
- package/dist/dsl/schema/index.js.map +1 -0
- package/dist/dsl/schema/load.d.ts +41 -0
- package/dist/dsl/schema/load.d.ts.map +1 -0
- package/dist/dsl/schema/load.js +119 -0
- package/dist/dsl/schema/load.js.map +1 -0
- package/dist/dsl/schema/store.d.ts +137 -0
- package/dist/dsl/schema/store.d.ts.map +1 -0
- package/dist/dsl/schema/store.js +204 -0
- package/dist/dsl/schema/store.js.map +1 -0
- package/dist/dsl/schema/structured-entities.d.ts +21 -0
- package/dist/dsl/schema/structured-entities.d.ts.map +1 -0
- package/dist/dsl/schema/structured-entities.js +39 -0
- package/dist/dsl/schema/structured-entities.js.map +1 -0
- package/dist/dsl/schema/summary.d.ts +51 -0
- package/dist/dsl/schema/summary.d.ts.map +1 -0
- package/dist/dsl/schema/summary.js +61 -0
- package/dist/dsl/schema/summary.js.map +1 -0
- package/dist/dsl/schema/types.d.ts +36 -0
- package/dist/dsl/schema/types.d.ts.map +1 -0
- package/dist/dsl/schema/types.js +6 -0
- package/dist/dsl/schema/types.js.map +1 -0
- package/dist/dsl/schema/validation.d.ts +21 -0
- package/dist/dsl/schema/validation.d.ts.map +1 -0
- package/dist/dsl/schema/validation.js +44 -0
- package/dist/dsl/schema/validation.js.map +1 -0
- package/dist/dsl/types/buckets.d.ts +18 -0
- package/dist/dsl/types/buckets.d.ts.map +1 -0
- package/dist/dsl/types/buckets.js +6 -0
- package/dist/dsl/types/buckets.js.map +1 -0
- package/dist/dsl/types/common.d.ts +91 -0
- package/dist/dsl/types/common.d.ts.map +1 -0
- package/dist/dsl/types/common.js +6 -0
- package/dist/dsl/types/common.js.map +1 -0
- package/dist/dsl/types/entities.d.ts +35 -0
- package/dist/dsl/types/entities.d.ts.map +1 -0
- package/dist/dsl/types/entities.js +11 -0
- package/dist/dsl/types/entities.js.map +1 -0
- package/dist/dsl/types/index.d.ts +12 -0
- package/dist/dsl/types/index.d.ts.map +1 -0
- package/dist/dsl/types/index.js +7 -0
- package/dist/dsl/types/index.js.map +1 -0
- package/dist/dsl/types/return-clauses.d.ts +107 -0
- package/dist/dsl/types/return-clauses.d.ts.map +1 -0
- package/dist/dsl/types/return-clauses.js +22 -0
- package/dist/dsl/types/return-clauses.js.map +1 -0
- package/dist/dsl/types/special-functions.d.ts +175 -0
- package/dist/dsl/types/special-functions.d.ts.map +1 -0
- package/dist/dsl/types/special-functions.js +25 -0
- package/dist/dsl/types/special-functions.js.map +1 -0
- package/dist/dsl/types/vocabulary.d.ts +21 -0
- package/dist/dsl/types/vocabulary.d.ts.map +1 -0
- package/dist/dsl/types/vocabulary.js +44 -0
- package/dist/dsl/types/vocabulary.js.map +1 -0
- package/dist/dsl/usage-policy.d.ts +87 -0
- package/dist/dsl/usage-policy.d.ts.map +1 -0
- package/dist/dsl/usage-policy.js +133 -0
- package/dist/dsl/usage-policy.js.map +1 -0
- package/dist/dsl/utils/escape.d.ts +24 -0
- package/dist/dsl/utils/escape.d.ts.map +1 -0
- package/dist/dsl/utils/escape.js +124 -0
- package/dist/dsl/utils/escape.js.map +1 -0
- package/dist/dsl/utils/sanitize.d.ts +13 -0
- package/dist/dsl/utils/sanitize.d.ts.map +1 -0
- package/dist/dsl/utils/sanitize.js +19 -0
- package/dist/dsl/utils/sanitize.js.map +1 -0
- package/dist/dsl/utils/string-utils.d.ts +35 -0
- package/dist/dsl/utils/string-utils.d.ts.map +1 -0
- package/dist/dsl/utils/string-utils.js +145 -0
- package/dist/dsl/utils/string-utils.js.map +1 -0
- package/dist/dsl/utils/validate-field-name.d.ts +11 -0
- package/dist/dsl/utils/validate-field-name.d.ts.map +1 -0
- package/dist/dsl/utils/validate-field-name.js +18 -0
- package/dist/dsl/utils/validate-field-name.js.map +1 -0
- package/dist/examples/dsl-examples.d.ts +11 -0
- package/dist/examples/dsl-examples.d.ts.map +1 -0
- package/dist/examples/dsl-examples.js +49 -0
- package/dist/examples/dsl-examples.js.map +1 -0
- package/dist/examples/usage-scenarios.d.ts +22 -0
- package/dist/examples/usage-scenarios.d.ts.map +1 -0
- package/dist/examples/usage-scenarios.js +173 -0
- package/dist/examples/usage-scenarios.js.map +1 -0
- package/dist/funder-org-names.d.ts +12 -0
- package/dist/funder-org-names.d.ts.map +1 -0
- package/dist/funder-org-names.js +30 -0
- package/dist/funder-org-names.js.map +1 -0
- package/dist/http-main.d.ts +7 -0
- package/dist/http-main.d.ts.map +1 -0
- package/dist/http-main.js +36 -0
- package/dist/http-main.js.map +1 -0
- package/dist/http.d.ts +29 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +173 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/kwq/build-query.d.ts +26 -0
- package/dist/kwq/build-query.d.ts.map +1 -0
- package/dist/kwq/build-query.js +37 -0
- package/dist/kwq/build-query.js.map +1 -0
- package/dist/kwq/concept-groups.d.ts +49 -0
- package/dist/kwq/concept-groups.d.ts.map +1 -0
- package/dist/kwq/concept-groups.js +55 -0
- package/dist/kwq/concept-groups.js.map +1 -0
- package/dist/kwq/dsl-assembler.d.ts +28 -0
- package/dist/kwq/dsl-assembler.d.ts.map +1 -0
- package/dist/kwq/dsl-assembler.js +124 -0
- package/dist/kwq/dsl-assembler.js.map +1 -0
- package/dist/kwq/ensemble.d.ts +38 -0
- package/dist/kwq/ensemble.d.ts.map +1 -0
- package/dist/kwq/ensemble.js +107 -0
- package/dist/kwq/ensemble.js.map +1 -0
- package/dist/kwq/index.d.ts +16 -0
- package/dist/kwq/index.d.ts.map +1 -0
- package/dist/kwq/index.js +14 -0
- package/dist/kwq/index.js.map +1 -0
- package/dist/kwq/types.d.ts +120 -0
- package/dist/kwq/types.d.ts.map +1 -0
- package/dist/kwq/types.js +11 -0
- package/dist/kwq/types.js.map +1 -0
- package/dist/lambda.d.ts +10 -0
- package/dist/lambda.d.ts.map +1 -0
- package/dist/lambda.js +4 -0
- package/dist/lambda.js.map +1 -0
- package/dist/main.d.ts +8 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +33 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/batch-fetch.d.ts +89 -0
- package/dist/mcp/batch-fetch.d.ts.map +1 -0
- package/dist/mcp/batch-fetch.js +178 -0
- package/dist/mcp/batch-fetch.js.map +1 -0
- package/dist/mcp/examples/dsl-examples.d.ts +21 -0
- package/dist/mcp/examples/dsl-examples.d.ts.map +1 -0
- package/dist/mcp/examples/dsl-examples.js +61 -0
- package/dist/mcp/examples/dsl-examples.js.map +1 -0
- package/dist/mcp/examples/usage-scenarios.d.ts +20 -0
- package/dist/mcp/examples/usage-scenarios.d.ts.map +1 -0
- package/dist/mcp/examples/usage-scenarios.js +169 -0
- package/dist/mcp/examples/usage-scenarios.js.map +1 -0
- package/dist/mcp/export-format.d.ts +43 -0
- package/dist/mcp/export-format.d.ts.map +1 -0
- package/dist/mcp/export-format.js +92 -0
- package/dist/mcp/export-format.js.map +1 -0
- package/dist/mcp/funder-org-names.d.ts +12 -0
- package/dist/mcp/funder-org-names.d.ts.map +1 -0
- package/dist/mcp/funder-org-names.js +30 -0
- package/dist/mcp/funder-org-names.js.map +1 -0
- package/dist/mcp/http-server.d.ts +20 -0
- package/dist/mcp/http-server.d.ts.map +1 -0
- package/dist/mcp/http-server.js +118 -0
- package/dist/mcp/http-server.js.map +1 -0
- package/dist/mcp/middleware/field-aliases.d.ts +97 -0
- package/dist/mcp/middleware/field-aliases.d.ts.map +1 -0
- package/dist/mcp/middleware/field-aliases.js +208 -0
- package/dist/mcp/middleware/field-aliases.js.map +1 -0
- package/dist/mcp/resources/schema.d.ts +13 -0
- package/dist/mcp/resources/schema.d.ts.map +1 -0
- package/dist/mcp/resources/schema.js +262 -0
- package/dist/mcp/resources/schema.js.map +1 -0
- package/dist/mcp/schema/context.d.ts +10 -0
- package/dist/mcp/schema/context.d.ts.map +1 -0
- package/dist/mcp/schema/context.js +6 -0
- package/dist/mcp/schema/context.js.map +1 -0
- package/dist/mcp/schema/index.d.ts +8 -0
- package/dist/mcp/schema/index.d.ts.map +1 -0
- package/dist/mcp/schema/index.js +6 -0
- package/dist/mcp/schema/index.js.map +1 -0
- package/dist/mcp/server.d.ts +56 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +168 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/shared-schema.d.ts +13 -0
- package/dist/mcp/shared-schema.d.ts.map +1 -0
- package/dist/mcp/shared-schema.js +28 -0
- package/dist/mcp/shared-schema.js.map +1 -0
- package/dist/mcp/tools/analytics-filters.d.ts +18 -0
- package/dist/mcp/tools/analytics-filters.d.ts.map +1 -0
- package/dist/mcp/tools/analytics-filters.js +35 -0
- package/dist/mcp/tools/analytics-filters.js.map +1 -0
- package/dist/mcp/tools/analytics.d.ts +16 -0
- package/dist/mcp/tools/analytics.d.ts.map +1 -0
- package/dist/mcp/tools/analytics.js +381 -0
- package/dist/mcp/tools/analytics.js.map +1 -0
- package/dist/mcp/tools/concept-profile.d.ts +14 -0
- package/dist/mcp/tools/concept-profile.d.ts.map +1 -0
- package/dist/mcp/tools/concept-profile.js +130 -0
- package/dist/mcp/tools/concept-profile.js.map +1 -0
- package/dist/mcp/tools/dataset-profile.d.ts +15 -0
- package/dist/mcp/tools/dataset-profile.d.ts.map +1 -0
- package/dist/mcp/tools/dataset-profile.js +194 -0
- package/dist/mcp/tools/dataset-profile.js.map +1 -0
- package/dist/mcp/tools/fetch-search-pages.d.ts +15 -0
- package/dist/mcp/tools/fetch-search-pages.d.ts.map +1 -0
- package/dist/mcp/tools/fetch-search-pages.js +237 -0
- package/dist/mcp/tools/fetch-search-pages.js.map +1 -0
- package/dist/mcp/tools/functions.d.ts +14 -0
- package/dist/mcp/tools/functions.d.ts.map +1 -0
- package/dist/mcp/tools/functions.js +87 -0
- package/dist/mcp/tools/functions.js.map +1 -0
- package/dist/mcp/tools/impact-chain.d.ts +15 -0
- package/dist/mcp/tools/impact-chain.d.ts.map +1 -0
- package/dist/mcp/tools/impact-chain.js +96 -0
- package/dist/mcp/tools/impact-chain.js.map +1 -0
- package/dist/mcp/tools/kwq.d.ts +19 -0
- package/dist/mcp/tools/kwq.d.ts.map +1 -0
- package/dist/mcp/tools/kwq.js +156 -0
- package/dist/mcp/tools/kwq.js.map +1 -0
- package/dist/mcp/tools/lookup.d.ts +14 -0
- package/dist/mcp/tools/lookup.d.ts.map +1 -0
- package/dist/mcp/tools/lookup.js +146 -0
- package/dist/mcp/tools/lookup.js.map +1 -0
- package/dist/mcp/tools/policy-profile.d.ts +15 -0
- package/dist/mcp/tools/policy-profile.d.ts.map +1 -0
- package/dist/mcp/tools/policy-profile.js +174 -0
- package/dist/mcp/tools/policy-profile.js.map +1 -0
- package/dist/mcp/tools/profile.d.ts +14 -0
- package/dist/mcp/tools/profile.d.ts.map +1 -0
- package/dist/mcp/tools/profile.js +349 -0
- package/dist/mcp/tools/profile.js.map +1 -0
- package/dist/mcp/tools/query.d.ts +16 -0
- package/dist/mcp/tools/query.d.ts.map +1 -0
- package/dist/mcp/tools/query.js +75 -0
- package/dist/mcp/tools/query.js.map +1 -0
- package/dist/mcp/tools/schema.d.ts +23 -0
- package/dist/mcp/tools/schema.d.ts.map +1 -0
- package/dist/mcp/tools/schema.js +124 -0
- package/dist/mcp/tools/schema.js.map +1 -0
- package/dist/mcp/tools/search-entity-metadata.d.ts +18 -0
- package/dist/mcp/tools/search-entity-metadata.d.ts.map +1 -0
- package/dist/mcp/tools/search-entity-metadata.js +221 -0
- package/dist/mcp/tools/search-entity-metadata.js.map +1 -0
- package/dist/mcp/tools/search-input.d.ts +58 -0
- package/dist/mcp/tools/search-input.d.ts.map +1 -0
- package/dist/mcp/tools/search-input.js +76 -0
- package/dist/mcp/tools/search-input.js.map +1 -0
- package/dist/mcp/tools/search.d.ts +24 -0
- package/dist/mcp/tools/search.d.ts.map +1 -0
- package/dist/mcp/tools/search.js +115 -0
- package/dist/mcp/tools/search.js.map +1 -0
- package/dist/mcp/utils.d.ts +82 -0
- package/dist/mcp/utils.d.ts.map +1 -0
- package/dist/mcp/utils.js +126 -0
- package/dist/mcp/utils.js.map +1 -0
- package/dist/middleware/field-aliases.d.ts +97 -0
- package/dist/middleware/field-aliases.d.ts.map +1 -0
- package/dist/middleware/field-aliases.js +208 -0
- package/dist/middleware/field-aliases.js.map +1 -0
- package/dist/resources/schema.d.ts +13 -0
- package/dist/resources/schema.d.ts.map +1 -0
- package/dist/resources/schema.js +204 -0
- package/dist/resources/schema.js.map +1 -0
- package/dist/schema/context.d.ts +10 -0
- package/dist/schema/context.d.ts.map +1 -0
- package/dist/schema/context.js +6 -0
- package/dist/schema/context.js.map +1 -0
- package/dist/schema/index.d.ts +9 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +6 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/load.d.ts +43 -0
- package/dist/schema/load.d.ts.map +1 -0
- package/dist/schema/load.js +139 -0
- package/dist/schema/load.js.map +1 -0
- package/dist/schema/store.d.ts +112 -0
- package/dist/schema/store.d.ts.map +1 -0
- package/dist/schema/store.js +173 -0
- package/dist/schema/store.js.map +1 -0
- package/dist/schema/structured-entities.d.ts +21 -0
- package/dist/schema/structured-entities.d.ts.map +1 -0
- package/dist/schema/structured-entities.js +39 -0
- package/dist/schema/structured-entities.js.map +1 -0
- package/dist/schema/types.d.ts +36 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +6 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/server.d.ts +57 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +144 -0
- package/dist/server.js.map +1 -0
- package/dist/telemetry.d.ts +172 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +233 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tools/analytics-filters.d.ts +18 -0
- package/dist/tools/analytics-filters.d.ts.map +1 -0
- package/dist/tools/analytics-filters.js +35 -0
- package/dist/tools/analytics-filters.js.map +1 -0
- package/dist/tools/analytics.d.ts +16 -0
- package/dist/tools/analytics.d.ts.map +1 -0
- package/dist/tools/analytics.js +381 -0
- package/dist/tools/analytics.js.map +1 -0
- package/dist/tools/concept-profile.d.ts +14 -0
- package/dist/tools/concept-profile.d.ts.map +1 -0
- package/dist/tools/concept-profile.js +130 -0
- package/dist/tools/concept-profile.js.map +1 -0
- package/dist/tools/dataset-profile.d.ts +15 -0
- package/dist/tools/dataset-profile.d.ts.map +1 -0
- package/dist/tools/dataset-profile.js +194 -0
- package/dist/tools/dataset-profile.js.map +1 -0
- package/dist/tools/functions.d.ts +14 -0
- package/dist/tools/functions.d.ts.map +1 -0
- package/dist/tools/functions.js +294 -0
- package/dist/tools/functions.js.map +1 -0
- package/dist/tools/impact-chain.d.ts +15 -0
- package/dist/tools/impact-chain.d.ts.map +1 -0
- package/dist/tools/impact-chain.js +96 -0
- package/dist/tools/impact-chain.js.map +1 -0
- package/dist/tools/kwq.d.ts +19 -0
- package/dist/tools/kwq.d.ts.map +1 -0
- package/dist/tools/kwq.js +156 -0
- package/dist/tools/kwq.js.map +1 -0
- package/dist/tools/lookup.d.ts +14 -0
- package/dist/tools/lookup.d.ts.map +1 -0
- package/dist/tools/lookup.js +213 -0
- package/dist/tools/lookup.js.map +1 -0
- package/dist/tools/policy-profile.d.ts +15 -0
- package/dist/tools/policy-profile.d.ts.map +1 -0
- package/dist/tools/policy-profile.js +174 -0
- package/dist/tools/policy-profile.js.map +1 -0
- package/dist/tools/profile.d.ts +14 -0
- package/dist/tools/profile.d.ts.map +1 -0
- package/dist/tools/profile.js +349 -0
- package/dist/tools/profile.js.map +1 -0
- package/dist/tools/query.d.ts +16 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +53 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/schema.d.ts +23 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/schema.js +124 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/tools/search-entity-metadata.d.ts +18 -0
- package/dist/tools/search-entity-metadata.d.ts.map +1 -0
- package/dist/tools/search-entity-metadata.js +221 -0
- package/dist/tools/search-entity-metadata.js.map +1 -0
- package/dist/tools/search.d.ts +23 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +110 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/solr-fields.d.ts +39 -0
- package/dist/tools/solr-fields.d.ts.map +1 -0
- package/dist/tools/solr-fields.js +40 -0
- package/dist/tools/solr-fields.js.map +1 -0
- package/dist/utils.d.ts +70 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +104 -0
- package/dist/utils.js.map +1 -0
- package/package.json +41 -0
- package/src/client/auth/config-loaders.ts +81 -0
- package/src/client/auth/decorators/index.ts +7 -0
- package/src/client/auth/decorators/with-caching.ts +237 -0
- package/src/client/auth/factory.ts +34 -0
- package/src/client/auth/index.ts +6 -0
- package/src/client/auth/jwt-utils.ts +36 -0
- package/src/client/auth/node.ts +16 -0
- package/src/client/auth/providers/index.ts +1 -0
- package/src/client/auth/providers/jwt-auth-provider.ts +153 -0
- package/src/client/auth/schemas.ts +25 -0
- package/src/client/auth/types.ts +30 -0
- package/src/client/command.ts +54 -0
- package/src/client/config/index.ts +15 -0
- package/src/client/config/loader.ts +155 -0
- package/src/client/config/node.ts +22 -0
- package/src/client/config/schemas.ts +28 -0
- package/src/client/config/types.ts +39 -0
- package/src/client/config/unified-loader.ts +118 -0
- package/src/client/config.ts +69 -0
- package/src/client/deployment-config.ts +98 -0
- package/src/client/errors.ts +295 -0
- package/src/client/http-client.ts +577 -0
- package/src/client/index.ts +68 -0
- package/src/client/internal-dsl-client.ts +128 -0
- package/src/client/rate-limiter.ts +139 -0
- package/src/client/resolve-api-key-user.ts +99 -0
- package/src/client/types.ts +44 -0
- package/src/dsl/client.ts +538 -0
- package/src/dsl/commands/apply-filters.ts +35 -0
- package/src/dsl/commands/facet/index.ts +12 -0
- package/src/dsl/commands/facet/schemas.ts +101 -0
- package/src/dsl/commands/functions/ClassifyCommand.ts +129 -0
- package/src/dsl/commands/functions/ExtractAffiliationsCommand.ts +163 -0
- package/src/dsl/commands/functions/ExtractConceptsCommand.ts +141 -0
- package/src/dsl/commands/functions/ExtractGrantsCommand.ts +112 -0
- package/src/dsl/commands/functions/index.ts +31 -0
- package/src/dsl/commands/index.ts +29 -0
- package/src/dsl/commands/validate-input.ts +29 -0
- package/src/dsl/create-client.ts +80 -0
- package/src/dsl/fluent-query-builder.ts +684 -0
- package/src/dsl/index.ts +206 -0
- package/src/dsl/pagination.ts +202 -0
- package/src/dsl/query-builder.ts +1120 -0
- package/src/dsl/response-parser.ts +299 -0
- package/src/dsl/schema/cache.ts +124 -0
- package/src/dsl/schema/extract.ts +44 -0
- package/src/dsl/schema/index.ts +50 -0
- package/src/dsl/schema/load.ts +160 -0
- package/src/dsl/schema/store.ts +253 -0
- package/src/dsl/schema/structured-entities.ts +44 -0
- package/src/dsl/schema/summary.ts +94 -0
- package/src/dsl/schema/types.ts +40 -0
- package/src/dsl/schema/validation.ts +56 -0
- package/src/dsl/types/buckets.ts +22 -0
- package/src/dsl/types/common.ts +99 -0
- package/src/dsl/types/entities.ts +43 -0
- package/src/dsl/types/index.ts +63 -0
- package/src/dsl/types/return-clauses.ts +147 -0
- package/src/dsl/types/special-functions.ts +220 -0
- package/src/dsl/types/vocabulary.ts +82 -0
- package/src/dsl/usage-policy.ts +219 -0
- package/src/dsl/utils/escape.ts +123 -0
- package/src/dsl/utils/sanitize.ts +21 -0
- package/src/dsl/utils/string-utils.ts +147 -0
- package/src/dsl/utils/validate-field-name.ts +19 -0
- package/src/http-main.ts +41 -0
- package/src/index.ts +14 -0
- package/src/main.ts +40 -0
- package/src/mcp/batch-fetch.ts +255 -0
- package/src/mcp/examples/dsl-examples.ts +66 -0
- package/src/mcp/examples/usage-scenarios.ts +185 -0
- package/src/mcp/export-format.ts +113 -0
- package/src/mcp/funder-org-names.ts +30 -0
- package/src/mcp/http-server.ts +148 -0
- package/src/mcp/middleware/field-aliases.ts +274 -0
- package/src/mcp/resources/schema.ts +364 -0
- package/src/mcp/schema/context.ts +11 -0
- package/src/mcp/schema/index.ts +38 -0
- package/src/mcp/server.ts +247 -0
- package/src/mcp/shared-schema.ts +34 -0
- package/src/mcp/tools/analytics-filters.ts +46 -0
- package/src/mcp/tools/analytics.ts +453 -0
- package/src/mcp/tools/fetch-search-pages.ts +329 -0
- package/src/mcp/tools/functions.ts +105 -0
- package/src/mcp/tools/lookup.ts +182 -0
- package/src/mcp/tools/query.ts +91 -0
- package/src/mcp/tools/schema.ts +147 -0
- package/src/mcp/tools/search-entity-metadata.ts +253 -0
- package/src/mcp/tools/search-input.ts +87 -0
- package/src/mcp/tools/search.ts +168 -0
- package/src/mcp/utils.ts +199 -0
- package/test/client/auth/config-loaders.test.ts +92 -0
- package/test/client/auth/decorators/with-caching.test.ts +482 -0
- package/test/client/auth/factory.test.ts +68 -0
- package/test/client/auth/providers/jwt-auth-provider.test.ts +398 -0
- package/test/client/auth/types.test.ts +18 -0
- package/test/client/command.test.ts +163 -0
- package/test/client/config/loader.test.ts +377 -0
- package/test/client/config/unified-loader.test.ts +136 -0
- package/test/client/deployment-config.test.ts +69 -0
- package/test/client/errors/index.test.ts +408 -0
- package/test/client/helpers/mock-fetch.ts +42 -0
- package/test/client/http-client.test.ts +1148 -0
- package/test/client/integration/auth-providers.integration.test.ts +186 -0
- package/test/client/internal-dsl-client.test.ts +76 -0
- package/test/client/rate-limiter.test.ts +204 -0
- package/test/client/resolve-api-key-user.test.ts +87 -0
- package/test/dsl/client.special-functions.test.ts +387 -0
- package/test/dsl/client.test.ts +457 -0
- package/test/dsl/commands/apply-filters.test.ts +147 -0
- package/test/dsl/commands/functions/ClassifyCommand.test.ts +170 -0
- package/test/dsl/commands/functions/ExtractAffiliationsCommand.test.ts +306 -0
- package/test/dsl/commands/functions/ExtractConceptsCommand.test.ts +136 -0
- package/test/dsl/commands/functions/ExtractGrantsCommand.test.ts +202 -0
- package/test/dsl/commands/search/filters.test.ts +42 -0
- package/test/dsl/fluent-query-builder.test.ts +865 -0
- package/test/dsl/integration/client.integration.test.ts +294 -0
- package/test/dsl/integration/facet-api.integration.test.ts +276 -0
- package/test/dsl/integration/field-existence.integration.test.ts +148 -0
- package/test/dsl/integration/fluent-api-docs-examples.integration.test.ts +988 -0
- package/test/dsl/integration/fluent-api.integration.test.ts +615 -0
- package/test/dsl/integration/search-helpers.ts +27 -0
- package/test/dsl/integration/special-functions.integration.test.ts +224 -0
- package/test/dsl/integration/test-config.ts +52 -0
- package/test/dsl/pagination.test.ts +86 -0
- package/test/dsl/query-builder.test.ts +1775 -0
- package/test/dsl/response-parser.test.ts +520 -0
- package/test/dsl/schema/cache.test.ts +53 -0
- package/test/dsl/schema/load.test.ts +165 -0
- package/test/dsl/schema/summary.test.ts +61 -0
- package/test/dsl/schema/validation.test.ts +46 -0
- package/test/dsl/time-series.test.ts +135 -0
- package/test/dsl/unnest.test.ts +151 -0
- package/test/dsl/usage-policy.test.ts +100 -0
- package/test/dsl/utils/escape.property.test.ts +109 -0
- package/test/dsl/utils/escape.test.ts +140 -0
- package/test/dsl/utils/validate-field-name.property.test.ts +99 -0
- package/test/dsl/utils/validate-field-name.test.ts +55 -0
- package/test/examples/dsl-examples.test.ts +29 -0
- package/test/examples/usage-scenarios.test.ts +90 -0
- package/test/fixtures/describe-schema.json +4000 -0
- package/test/funder-org-names.test.ts +20 -0
- package/test/helpers/schema-fixture.ts +18 -0
- package/test/helpers/tool-test-harness.ts +114 -0
- package/test/integration/assertions.ts +263 -0
- package/test/integration/env.ts +108 -0
- package/test/integration/harness.ts +307 -0
- package/test/integration/hosted-client.ts +56 -0
- package/test/integration/hosted-mocks.ts +81 -0
- package/test/integration/hosted-run.ts +86 -0
- package/test/integration/hosted.e2e.test.ts +101 -0
- package/test/integration/reporter.ts +100 -0
- package/test/integration/run.ts +74 -0
- package/test/integration/suites/analytics.integration.ts +72 -0
- package/test/integration/suites/functions.integration.ts +37 -0
- package/test/integration/suites/hosted-smoke.integration.ts +38 -0
- package/test/integration/suites/index.ts +22 -0
- package/test/integration/suites/lookup.integration.ts +57 -0
- package/test/integration/suites/query.integration.ts +49 -0
- package/test/integration/suites/resources.integration.ts +68 -0
- package/test/integration/suites/search.integration.ts +80 -0
- package/test/integration/types.ts +92 -0
- package/test/integration/verify-usage-scenarios.ts +162 -0
- package/test/mcp/export-format.test.ts +67 -0
- package/test/mcp/http-server.test.ts +80 -0
- package/test/middleware/field-aliases.test.ts +287 -0
- package/test/resources/schema.test.ts +28 -0
- package/test/schema/describe-contract.test.ts +24 -0
- package/test/schema/store.test.ts +45 -0
- package/test/server.test.ts +170 -0
- package/test/tools/analytics-filters.test.ts +35 -0
- package/test/tools/analytics.test.ts +419 -0
- package/test/tools/fetch-search-pages.test.ts +200 -0
- package/test/tools/functions.test.ts +153 -0
- package/test/tools/lookup.test.ts +296 -0
- package/test/tools/query.test.ts +100 -0
- package/test/tools/schema.test.ts +37 -0
- package/test/tools/search.test.ts +415 -0
- package/test/utils.test.ts +45 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AuthProvider } from "../../src/client/auth/types.js";
|
|
3
|
+
import {
|
|
4
|
+
DimensionsError,
|
|
5
|
+
NetworkError,
|
|
6
|
+
QuerySyntaxError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
ServerError,
|
|
9
|
+
TimeoutError,
|
|
10
|
+
} from "../../src/client/errors.js";
|
|
11
|
+
import { HttpClient } from "../../src/client/http-client.js";
|
|
12
|
+
import { RateLimiter } from "../../src/client/rate-limiter.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a mock auth provider for testing.
|
|
16
|
+
*/
|
|
17
|
+
function createMockAuthProvider(token: string = "eyJhbGc.eyJzdWI.dGVzdA"): AuthProvider {
|
|
18
|
+
return {
|
|
19
|
+
type: "jwt",
|
|
20
|
+
initialize: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
getHeaders: vi.fn().mockResolvedValue({ Authorization: `JWT ${token}` }),
|
|
22
|
+
refresh: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
invalidate: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("HttpClient", () => {
|
|
28
|
+
let client: HttpClient;
|
|
29
|
+
let mockAuthProvider: AuthProvider;
|
|
30
|
+
const originalFetch = globalThis.fetch;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockAuthProvider = createMockAuthProvider();
|
|
34
|
+
client = new HttpClient({
|
|
35
|
+
baseUrl: "https://app.dimensions.ai",
|
|
36
|
+
authProvider: mockAuthProvider,
|
|
37
|
+
timeout: 5000,
|
|
38
|
+
maxRetries: 3,
|
|
39
|
+
retryDelay: 10, // Short delay for faster tests
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
globalThis.fetch = originalFetch;
|
|
45
|
+
vi.restoreAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("successful requests", () => {
|
|
49
|
+
it("sends DSL query with correct headers", async () => {
|
|
50
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
51
|
+
ok: true,
|
|
52
|
+
json: () =>
|
|
53
|
+
Promise.resolve({
|
|
54
|
+
_stats: { total_count: 1 },
|
|
55
|
+
publications: [{ id: "pub.123", title: "Test Publication" }],
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
globalThis.fetch = mockFetch;
|
|
59
|
+
|
|
60
|
+
await client.query('search publications for "test"');
|
|
61
|
+
|
|
62
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
63
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
64
|
+
expect(url).toBe("https://app.dimensions.ai/api/dsl/v2");
|
|
65
|
+
expect(options.method).toBe("POST");
|
|
66
|
+
expect(options.headers["Authorization"]).toBe("JWT eyJhbGc.eyJzdWI.dGVzdA");
|
|
67
|
+
expect(options.headers["Content-Type"]).toBe("application/x-www-form-urlencoded");
|
|
68
|
+
expect(options.body).toBe('search publications for "test"');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns parsed response", async () => {
|
|
72
|
+
const mockResponse = {
|
|
73
|
+
_stats: { total_count: 2 },
|
|
74
|
+
publications: [
|
|
75
|
+
{ id: "pub.1", title: "First" },
|
|
76
|
+
{ id: "pub.2", title: "Second" },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
81
|
+
ok: true,
|
|
82
|
+
json: () => Promise.resolve(mockResponse),
|
|
83
|
+
});
|
|
84
|
+
globalThis.fetch = mockFetch;
|
|
85
|
+
|
|
86
|
+
const result = await client.query('search publications for "test"');
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual(mockResponse);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("calls authProvider.getHeaders on each request", async () => {
|
|
92
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
93
|
+
ok: true,
|
|
94
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
95
|
+
});
|
|
96
|
+
globalThis.fetch = mockFetch;
|
|
97
|
+
|
|
98
|
+
await client.query("search publications");
|
|
99
|
+
await client.query("search publications");
|
|
100
|
+
|
|
101
|
+
expect(mockAuthProvider.getHeaders).toHaveBeenCalledTimes(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("query() without options still works (backward compat)", async () => {
|
|
105
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
106
|
+
ok: true,
|
|
107
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
108
|
+
});
|
|
109
|
+
globalThis.fetch = mockFetch;
|
|
110
|
+
|
|
111
|
+
const result = await client.query("search publications");
|
|
112
|
+
|
|
113
|
+
expect(result).toEqual({ _stats: { total_count: 0 } });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("query() with timeout option overrides default timeout", async () => {
|
|
117
|
+
const mockFetch = vi.fn().mockImplementation(
|
|
118
|
+
() =>
|
|
119
|
+
new Promise((resolve) =>
|
|
120
|
+
setTimeout(
|
|
121
|
+
() =>
|
|
122
|
+
resolve({
|
|
123
|
+
ok: true,
|
|
124
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
125
|
+
}),
|
|
126
|
+
10,
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
globalThis.fetch = mockFetch;
|
|
131
|
+
|
|
132
|
+
await client.query("search publications", "/api/dsl/v2", { timeout: 60000 });
|
|
133
|
+
|
|
134
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("query() with signal threads AbortSignal to fetch", async () => {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
140
|
+
ok: true,
|
|
141
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
142
|
+
});
|
|
143
|
+
globalThis.fetch = mockFetch;
|
|
144
|
+
|
|
145
|
+
await client.query("search publications", "/api/dsl/v2", {
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
150
|
+
const [, fetchOptions] = mockFetch.mock.calls[0];
|
|
151
|
+
// When an external signal is provided, AbortSignal.any is used to compose it
|
|
152
|
+
// with the internal timeout signal — so signal should be defined
|
|
153
|
+
expect(fetchOptions.signal).toBeDefined();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("retry behavior", () => {
|
|
158
|
+
it("retries on 429 rate limit error", async () => {
|
|
159
|
+
vi.spyOn(client as never, "delay" as never).mockResolvedValue(undefined as never);
|
|
160
|
+
|
|
161
|
+
const mockFetch = vi
|
|
162
|
+
.fn()
|
|
163
|
+
.mockResolvedValueOnce({
|
|
164
|
+
ok: false,
|
|
165
|
+
status: 429,
|
|
166
|
+
statusText: "Too Many Requests",
|
|
167
|
+
headers: new Headers({ "Retry-After": "1" }),
|
|
168
|
+
})
|
|
169
|
+
.mockResolvedValueOnce({
|
|
170
|
+
ok: true,
|
|
171
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
172
|
+
});
|
|
173
|
+
globalThis.fetch = mockFetch;
|
|
174
|
+
|
|
175
|
+
const result = await client.query("search publications");
|
|
176
|
+
|
|
177
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
178
|
+
expect(result).toEqual({ _stats: { total_count: 0 } });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("retries on 500 server error", async () => {
|
|
182
|
+
const mockFetch = vi
|
|
183
|
+
.fn()
|
|
184
|
+
.mockResolvedValueOnce({
|
|
185
|
+
ok: false,
|
|
186
|
+
status: 500,
|
|
187
|
+
statusText: "Internal Server Error",
|
|
188
|
+
})
|
|
189
|
+
.mockResolvedValueOnce({
|
|
190
|
+
ok: true,
|
|
191
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
192
|
+
});
|
|
193
|
+
globalThis.fetch = mockFetch;
|
|
194
|
+
|
|
195
|
+
const result = await client.query("search publications");
|
|
196
|
+
|
|
197
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
198
|
+
expect(result).toEqual({ _stats: { total_count: 0 } });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("retries on 502 bad gateway", async () => {
|
|
202
|
+
const mockFetch = vi
|
|
203
|
+
.fn()
|
|
204
|
+
.mockResolvedValueOnce({
|
|
205
|
+
ok: false,
|
|
206
|
+
status: 502,
|
|
207
|
+
statusText: "Bad Gateway",
|
|
208
|
+
})
|
|
209
|
+
.mockResolvedValueOnce({
|
|
210
|
+
ok: true,
|
|
211
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
212
|
+
});
|
|
213
|
+
globalThis.fetch = mockFetch;
|
|
214
|
+
|
|
215
|
+
const result = await client.query("search publications");
|
|
216
|
+
|
|
217
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
218
|
+
expect(result).toEqual({ _stats: { total_count: 0 } });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("retries on 503 service unavailable", async () => {
|
|
222
|
+
const mockFetch = vi
|
|
223
|
+
.fn()
|
|
224
|
+
.mockResolvedValueOnce({
|
|
225
|
+
ok: false,
|
|
226
|
+
status: 503,
|
|
227
|
+
statusText: "Service Unavailable",
|
|
228
|
+
})
|
|
229
|
+
.mockResolvedValueOnce({
|
|
230
|
+
ok: true,
|
|
231
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
232
|
+
});
|
|
233
|
+
globalThis.fetch = mockFetch;
|
|
234
|
+
|
|
235
|
+
const result = await client.query("search publications");
|
|
236
|
+
|
|
237
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
238
|
+
expect(result).toEqual({ _stats: { total_count: 0 } });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("exhausts retries and throws RateLimitError", async () => {
|
|
242
|
+
vi.spyOn(client as never, "delay" as never).mockResolvedValue(undefined as never);
|
|
243
|
+
|
|
244
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
245
|
+
ok: false,
|
|
246
|
+
status: 429,
|
|
247
|
+
statusText: "Too Many Requests",
|
|
248
|
+
headers: new Headers({ "Retry-After": "60" }),
|
|
249
|
+
});
|
|
250
|
+
globalThis.fetch = mockFetch;
|
|
251
|
+
|
|
252
|
+
await expect(client.query("search publications")).rejects.toThrow(RateLimitError);
|
|
253
|
+
|
|
254
|
+
// Initial + 3 retries = 4 calls
|
|
255
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("exhausts retries and throws ServerError", async () => {
|
|
259
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
260
|
+
ok: false,
|
|
261
|
+
status: 500,
|
|
262
|
+
statusText: "Internal Server Error",
|
|
263
|
+
});
|
|
264
|
+
globalThis.fetch = mockFetch;
|
|
265
|
+
|
|
266
|
+
await expect(client.query("search publications")).rejects.toThrow(ServerError);
|
|
267
|
+
|
|
268
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("uses client rate limiter delay for 429 responses", async () => {
|
|
272
|
+
const rateLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60_000 });
|
|
273
|
+
const limitedClient = new HttpClient({
|
|
274
|
+
baseUrl: "https://app.dimensions.ai",
|
|
275
|
+
authProvider: mockAuthProvider,
|
|
276
|
+
timeout: 5000,
|
|
277
|
+
maxRetries: 1,
|
|
278
|
+
retryDelay: 10,
|
|
279
|
+
rateLimiter,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const delaySpy = vi
|
|
283
|
+
.spyOn(limitedClient as never, "delay" as never)
|
|
284
|
+
.mockResolvedValue(undefined as never);
|
|
285
|
+
|
|
286
|
+
const mockFetch = vi
|
|
287
|
+
.fn()
|
|
288
|
+
.mockResolvedValueOnce({
|
|
289
|
+
ok: false,
|
|
290
|
+
status: 429,
|
|
291
|
+
statusText: "Too Many Requests",
|
|
292
|
+
headers: new Headers(),
|
|
293
|
+
})
|
|
294
|
+
.mockResolvedValueOnce({
|
|
295
|
+
ok: true,
|
|
296
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
297
|
+
});
|
|
298
|
+
globalThis.fetch = mockFetch;
|
|
299
|
+
|
|
300
|
+
await limitedClient.query("search publications");
|
|
301
|
+
|
|
302
|
+
expect(delaySpy).toHaveBeenCalledTimes(1);
|
|
303
|
+
expect(delaySpy).toHaveBeenCalledWith(2050);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("uses exponential backoff for 5xx errors", async () => {
|
|
307
|
+
const delaySpy = vi
|
|
308
|
+
.spyOn(client as never, "delay" as never)
|
|
309
|
+
.mockResolvedValue(undefined as never);
|
|
310
|
+
|
|
311
|
+
const mockFetch = vi
|
|
312
|
+
.fn()
|
|
313
|
+
.mockResolvedValueOnce({
|
|
314
|
+
ok: false,
|
|
315
|
+
status: 500,
|
|
316
|
+
statusText: "Internal Server Error",
|
|
317
|
+
})
|
|
318
|
+
.mockResolvedValueOnce({
|
|
319
|
+
ok: false,
|
|
320
|
+
status: 500,
|
|
321
|
+
statusText: "Internal Server Error",
|
|
322
|
+
})
|
|
323
|
+
.mockResolvedValueOnce({
|
|
324
|
+
ok: true,
|
|
325
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
326
|
+
});
|
|
327
|
+
globalThis.fetch = mockFetch;
|
|
328
|
+
|
|
329
|
+
await client.query("search publications");
|
|
330
|
+
|
|
331
|
+
expect(delaySpy).toHaveBeenCalledTimes(2);
|
|
332
|
+
const firstDelay = delaySpy.mock.calls[0][0] as number;
|
|
333
|
+
const secondDelay = delaySpy.mock.calls[1][0] as number;
|
|
334
|
+
// With retryDelay=10, attempt 0: 10 * 2^0 * (0.5..1) = 5..10
|
|
335
|
+
// attempt 1: 10 * 2^1 * (0.5..1) = 10..20
|
|
336
|
+
expect(firstDelay).toBeGreaterThanOrEqual(5);
|
|
337
|
+
expect(firstDelay).toBeLessThanOrEqual(10);
|
|
338
|
+
expect(secondDelay).toBeGreaterThanOrEqual(10);
|
|
339
|
+
expect(secondDelay).toBeLessThanOrEqual(20);
|
|
340
|
+
// Second delay should be larger on average (exponential)
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("caps retry delay at 60 seconds", async () => {
|
|
344
|
+
const largeDelayClient = new HttpClient({
|
|
345
|
+
baseUrl: "https://app.dimensions.ai",
|
|
346
|
+
authProvider: mockAuthProvider,
|
|
347
|
+
timeout: 5000,
|
|
348
|
+
maxRetries: 1,
|
|
349
|
+
retryDelay: 100000, // Very large base delay
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const delaySpy = vi
|
|
353
|
+
.spyOn(largeDelayClient as never, "delay" as never)
|
|
354
|
+
.mockResolvedValue(undefined as never);
|
|
355
|
+
|
|
356
|
+
const mockFetch = vi
|
|
357
|
+
.fn()
|
|
358
|
+
.mockResolvedValueOnce({
|
|
359
|
+
ok: false,
|
|
360
|
+
status: 500,
|
|
361
|
+
statusText: "Internal Server Error",
|
|
362
|
+
})
|
|
363
|
+
.mockResolvedValueOnce({
|
|
364
|
+
ok: true,
|
|
365
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
366
|
+
});
|
|
367
|
+
globalThis.fetch = mockFetch;
|
|
368
|
+
|
|
369
|
+
await largeDelayClient.query("search publications");
|
|
370
|
+
|
|
371
|
+
expect(delaySpy).toHaveBeenCalledTimes(1);
|
|
372
|
+
expect(delaySpy.mock.calls[0][0] as number).toBeLessThanOrEqual(60000);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("caps rate-limit retry delay at 60 seconds", async () => {
|
|
376
|
+
const rateLimiter = new RateLimiter({ maxRequests: 1, windowMs: 120_000 });
|
|
377
|
+
rateLimiter.recordRequest();
|
|
378
|
+
vi.spyOn(rateLimiter, "waitIfNeeded").mockResolvedValue(undefined);
|
|
379
|
+
const limitedClient = new HttpClient({
|
|
380
|
+
baseUrl: "https://app.dimensions.ai",
|
|
381
|
+
authProvider: mockAuthProvider,
|
|
382
|
+
timeout: 5000,
|
|
383
|
+
maxRetries: 1,
|
|
384
|
+
retryDelay: 10,
|
|
385
|
+
rateLimiter,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const delaySpy = vi
|
|
389
|
+
.spyOn(limitedClient as never, "delay" as never)
|
|
390
|
+
.mockResolvedValue(undefined as never);
|
|
391
|
+
|
|
392
|
+
const mockFetch = vi
|
|
393
|
+
.fn()
|
|
394
|
+
.mockResolvedValueOnce({
|
|
395
|
+
ok: false,
|
|
396
|
+
status: 429,
|
|
397
|
+
statusText: "Too Many Requests",
|
|
398
|
+
headers: new Headers(),
|
|
399
|
+
})
|
|
400
|
+
.mockResolvedValueOnce({
|
|
401
|
+
ok: true,
|
|
402
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
403
|
+
});
|
|
404
|
+
globalThis.fetch = mockFetch;
|
|
405
|
+
|
|
406
|
+
await limitedClient.query("search publications");
|
|
407
|
+
|
|
408
|
+
expect(delaySpy).toHaveBeenCalledTimes(1);
|
|
409
|
+
expect(delaySpy).toHaveBeenCalledWith(60_000);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("non-retryable errors", () => {
|
|
414
|
+
it("does NOT retry on 400 syntax error", async () => {
|
|
415
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
416
|
+
ok: false,
|
|
417
|
+
status: 400,
|
|
418
|
+
statusText: "Bad Request",
|
|
419
|
+
json: () =>
|
|
420
|
+
Promise.resolve({
|
|
421
|
+
error: { message: "Query syntax error near 'for'" },
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
424
|
+
globalThis.fetch = mockFetch;
|
|
425
|
+
|
|
426
|
+
await expect(client.query("search publications for")).rejects.toThrow(QuerySyntaxError);
|
|
427
|
+
|
|
428
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("does NOT retry on 401 auth error", async () => {
|
|
432
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
433
|
+
ok: false,
|
|
434
|
+
status: 401,
|
|
435
|
+
statusText: "Unauthorized",
|
|
436
|
+
});
|
|
437
|
+
globalThis.fetch = mockFetch;
|
|
438
|
+
|
|
439
|
+
await expect(client.query("search publications")).rejects.toThrow("Authentication failed");
|
|
440
|
+
|
|
441
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("does NOT retry on 403 forbidden", async () => {
|
|
445
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
446
|
+
ok: false,
|
|
447
|
+
status: 403,
|
|
448
|
+
statusText: "Forbidden",
|
|
449
|
+
});
|
|
450
|
+
globalThis.fetch = mockFetch;
|
|
451
|
+
|
|
452
|
+
await expect(client.query("search publications")).rejects.toThrow("Access denied");
|
|
453
|
+
|
|
454
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe("timeout handling", () => {
|
|
459
|
+
it("throws TimeoutError when request times out", async () => {
|
|
460
|
+
const slowClient = new HttpClient({
|
|
461
|
+
baseUrl: "https://app.dimensions.ai",
|
|
462
|
+
authProvider: mockAuthProvider,
|
|
463
|
+
timeout: 50,
|
|
464
|
+
maxRetries: 0,
|
|
465
|
+
retryDelay: 100,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const mockFetch = vi.fn().mockImplementationOnce(() => {
|
|
469
|
+
const error = new Error("Aborted");
|
|
470
|
+
error.name = "AbortError";
|
|
471
|
+
return Promise.reject(error);
|
|
472
|
+
});
|
|
473
|
+
globalThis.fetch = mockFetch;
|
|
474
|
+
|
|
475
|
+
await expect(slowClient.query("search publications")).rejects.toThrow(TimeoutError);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe("signal combination", () => {
|
|
480
|
+
it("passes combined signal to fetch when caller signal is provided", async () => {
|
|
481
|
+
const controller = new AbortController();
|
|
482
|
+
|
|
483
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
484
|
+
ok: true,
|
|
485
|
+
json: () => Promise.resolve({ data: "result" }),
|
|
486
|
+
});
|
|
487
|
+
globalThis.fetch = mockFetch;
|
|
488
|
+
|
|
489
|
+
const noRetryClient = new HttpClient({
|
|
490
|
+
baseUrl: "https://app.dimensions.ai",
|
|
491
|
+
authProvider: mockAuthProvider,
|
|
492
|
+
maxRetries: 0,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await noRetryClient.get("/api/test", { signal: controller.signal });
|
|
496
|
+
|
|
497
|
+
const [, fetchOptions] = mockFetch.mock.calls[0];
|
|
498
|
+
// AbortSignal.any() returns a new composite signal, not the caller's raw signal
|
|
499
|
+
expect(fetchOptions.signal).not.toBe(controller.signal);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("maps caller signal abort to TimeoutError", async () => {
|
|
503
|
+
const controller = new AbortController();
|
|
504
|
+
controller.abort();
|
|
505
|
+
|
|
506
|
+
const mockFetch = vi.fn().mockImplementation(() => {
|
|
507
|
+
const error = new Error("Aborted");
|
|
508
|
+
error.name = "AbortError";
|
|
509
|
+
return Promise.reject(error);
|
|
510
|
+
});
|
|
511
|
+
globalThis.fetch = mockFetch;
|
|
512
|
+
|
|
513
|
+
const noRetryClient = new HttpClient({
|
|
514
|
+
baseUrl: "https://app.dimensions.ai",
|
|
515
|
+
authProvider: mockAuthProvider,
|
|
516
|
+
maxRetries: 0,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await expect(noRetryClient.get("/api/test", { signal: controller.signal })).rejects.toThrow(
|
|
520
|
+
TimeoutError,
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("maps internal timeout to TimeoutError when no caller signal provided", async () => {
|
|
525
|
+
const mockFetch = vi.fn().mockImplementation(() => {
|
|
526
|
+
const error = new Error("Aborted");
|
|
527
|
+
error.name = "AbortError";
|
|
528
|
+
return Promise.reject(error);
|
|
529
|
+
});
|
|
530
|
+
globalThis.fetch = mockFetch;
|
|
531
|
+
|
|
532
|
+
const noRetryClient = new HttpClient({
|
|
533
|
+
baseUrl: "https://app.dimensions.ai",
|
|
534
|
+
authProvider: mockAuthProvider,
|
|
535
|
+
timeout: 50,
|
|
536
|
+
maxRetries: 0,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await expect(noRetryClient.query("search publications")).rejects.toThrow(TimeoutError);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
describe("network error handling", () => {
|
|
544
|
+
it("throws NetworkError on fetch failure", async () => {
|
|
545
|
+
const noRetryClient = new HttpClient({
|
|
546
|
+
baseUrl: "https://app.dimensions.ai",
|
|
547
|
+
authProvider: mockAuthProvider,
|
|
548
|
+
timeout: 5000,
|
|
549
|
+
maxRetries: 0,
|
|
550
|
+
retryDelay: 10,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const mockFetch = vi.fn().mockRejectedValueOnce(new Error("Network failure"));
|
|
554
|
+
globalThis.fetch = mockFetch;
|
|
555
|
+
|
|
556
|
+
await expect(noRetryClient.query("search publications")).rejects.toThrow(NetworkError);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("retries network errors", async () => {
|
|
560
|
+
const mockFetch = vi
|
|
561
|
+
.fn()
|
|
562
|
+
.mockRejectedValueOnce(new Error("Network failure"))
|
|
563
|
+
.mockResolvedValueOnce({
|
|
564
|
+
ok: true,
|
|
565
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
566
|
+
});
|
|
567
|
+
globalThis.fetch = mockFetch;
|
|
568
|
+
|
|
569
|
+
const result = await client.query("search publications");
|
|
570
|
+
|
|
571
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
572
|
+
expect(result).toEqual({ _stats: { total_count: 0 } });
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe("rate limit error details", () => {
|
|
577
|
+
it("includes client rate-limit info when a rate limiter is configured", async () => {
|
|
578
|
+
const rateLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60_000 });
|
|
579
|
+
const noRetryClient = new HttpClient({
|
|
580
|
+
baseUrl: "https://app.dimensions.ai",
|
|
581
|
+
authProvider: mockAuthProvider,
|
|
582
|
+
timeout: 5000,
|
|
583
|
+
maxRetries: 0,
|
|
584
|
+
rateLimiter,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
588
|
+
ok: false,
|
|
589
|
+
status: 429,
|
|
590
|
+
statusText: "Too Many Requests",
|
|
591
|
+
headers: new Headers(),
|
|
592
|
+
});
|
|
593
|
+
globalThis.fetch = mockFetch;
|
|
594
|
+
|
|
595
|
+
const error = await noRetryClient.query("search publications").catch((e: unknown) => e);
|
|
596
|
+
|
|
597
|
+
expect(error).toBeInstanceOf(RateLimitError);
|
|
598
|
+
expect((error as RateLimitError).retryAfter).toBe(3);
|
|
599
|
+
expect((error as RateLimitError).clientRateLimit).toEqual({
|
|
600
|
+
remaining: 30,
|
|
601
|
+
retryAfterMs: 2050,
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("uses a conservative default delay without a rate limiter", async () => {
|
|
606
|
+
const noRetryClient = new HttpClient({
|
|
607
|
+
baseUrl: "https://app.dimensions.ai",
|
|
608
|
+
authProvider: mockAuthProvider,
|
|
609
|
+
maxRetries: 0,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
613
|
+
ok: false,
|
|
614
|
+
status: 429,
|
|
615
|
+
statusText: "Too Many Requests",
|
|
616
|
+
headers: new Headers(),
|
|
617
|
+
});
|
|
618
|
+
globalThis.fetch = mockFetch;
|
|
619
|
+
|
|
620
|
+
const error = await noRetryClient.query("search publications").catch((e: unknown) => e);
|
|
621
|
+
|
|
622
|
+
expect(error).toBeInstanceOf(RateLimitError);
|
|
623
|
+
expect((error as RateLimitError).retryAfter).toBe(2);
|
|
624
|
+
expect((error as RateLimitError).clientRateLimit).toEqual({
|
|
625
|
+
remaining: 0,
|
|
626
|
+
retryAfterMs: 2000,
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe("custom endpoint", () => {
|
|
632
|
+
it("uses custom endpoint when provided", async () => {
|
|
633
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
634
|
+
ok: true,
|
|
635
|
+
json: () => Promise.resolve({ result: "data" }),
|
|
636
|
+
});
|
|
637
|
+
globalThis.fetch = mockFetch;
|
|
638
|
+
|
|
639
|
+
await client.query("search publications", "/api/custom");
|
|
640
|
+
|
|
641
|
+
const [url] = mockFetch.mock.calls[0];
|
|
642
|
+
expect(url).toBe("https://app.dimensions.ai/api/custom");
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe("error handling edge cases", () => {
|
|
647
|
+
it("handles 400 error without JSON body", async () => {
|
|
648
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
649
|
+
ok: false,
|
|
650
|
+
status: 400,
|
|
651
|
+
statusText: "Bad Request",
|
|
652
|
+
json: () => Promise.reject(new Error("Not JSON")),
|
|
653
|
+
});
|
|
654
|
+
globalThis.fetch = mockFetch;
|
|
655
|
+
|
|
656
|
+
await expect(client.query("invalid query")).rejects.toThrow(QuerySyntaxError);
|
|
657
|
+
// Should use default message when JSON parsing fails
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("throws NetworkError for unknown error types", async () => {
|
|
661
|
+
const noRetryClient = new HttpClient({
|
|
662
|
+
baseUrl: "https://app.dimensions.ai",
|
|
663
|
+
authProvider: mockAuthProvider,
|
|
664
|
+
maxRetries: 0,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const mockFetch = vi.fn().mockRejectedValue("string error");
|
|
668
|
+
globalThis.fetch = mockFetch;
|
|
669
|
+
|
|
670
|
+
await expect(noRetryClient.query("search publications")).rejects.toThrow(
|
|
671
|
+
"Unknown network error",
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("maps 403 to generic DimensionsError", async () => {
|
|
676
|
+
const noRetryClient = new HttpClient({
|
|
677
|
+
baseUrl: "https://app.dimensions.ai",
|
|
678
|
+
authProvider: mockAuthProvider,
|
|
679
|
+
maxRetries: 0,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
683
|
+
ok: false,
|
|
684
|
+
status: 403,
|
|
685
|
+
statusText: "Forbidden",
|
|
686
|
+
});
|
|
687
|
+
globalThis.fetch = mockFetch;
|
|
688
|
+
|
|
689
|
+
const error = await noRetryClient.query("search publications").catch((e: unknown) => e);
|
|
690
|
+
|
|
691
|
+
expect(error).toBeInstanceOf(DimensionsError);
|
|
692
|
+
expect((error as DimensionsError).statusCode).toBe(403);
|
|
693
|
+
expect((error as DimensionsError).message).toBe("Access denied");
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("maps unknown 4xx status to generic DimensionsError", async () => {
|
|
697
|
+
const noRetryClient = new HttpClient({
|
|
698
|
+
baseUrl: "https://app.dimensions.ai",
|
|
699
|
+
authProvider: mockAuthProvider,
|
|
700
|
+
maxRetries: 0,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
704
|
+
ok: false,
|
|
705
|
+
status: 418,
|
|
706
|
+
statusText: "I'm a teapot",
|
|
707
|
+
});
|
|
708
|
+
globalThis.fetch = mockFetch;
|
|
709
|
+
|
|
710
|
+
const error = await noRetryClient.query("search publications").catch((e: unknown) => e);
|
|
711
|
+
|
|
712
|
+
expect(error).toBeInstanceOf(DimensionsError);
|
|
713
|
+
expect((error as DimensionsError).statusCode).toBe(418);
|
|
714
|
+
expect((error as DimensionsError).message).toContain("I'm a teapot");
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe("constructor validation", () => {
|
|
719
|
+
it("uses default values when not provided", () => {
|
|
720
|
+
const defaultClient = new HttpClient({
|
|
721
|
+
baseUrl: "https://example.com",
|
|
722
|
+
authProvider: mockAuthProvider,
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
expect(defaultClient).toBeInstanceOf(HttpClient);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("accepts custom configuration", () => {
|
|
729
|
+
const customClient = new HttpClient({
|
|
730
|
+
baseUrl: "https://custom.api.com",
|
|
731
|
+
authProvider: mockAuthProvider,
|
|
732
|
+
timeout: 60000,
|
|
733
|
+
maxRetries: 5,
|
|
734
|
+
retryDelay: 500,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
expect(customClient).toBeInstanceOf(HttpClient);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
describe("get method", () => {
|
|
742
|
+
it("sends GET request with query parameters", async () => {
|
|
743
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
744
|
+
ok: true,
|
|
745
|
+
json: () => Promise.resolve({ data: "result" }),
|
|
746
|
+
});
|
|
747
|
+
globalThis.fetch = mockFetch;
|
|
748
|
+
|
|
749
|
+
const result = await client.get<{ data: string }>("/api/test", {
|
|
750
|
+
params: { q: "search", limit: 10, active: true },
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
754
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
755
|
+
expect(url).toBe("https://app.dimensions.ai/api/test?q=search&limit=10&active=true");
|
|
756
|
+
expect(options.method).toBe("GET");
|
|
757
|
+
expect(result).toEqual({ data: "result" });
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it("sends GET request without params", async () => {
|
|
761
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
762
|
+
ok: true,
|
|
763
|
+
json: () => Promise.resolve({ data: "result" }),
|
|
764
|
+
});
|
|
765
|
+
globalThis.fetch = mockFetch;
|
|
766
|
+
|
|
767
|
+
await client.get("/api/test");
|
|
768
|
+
|
|
769
|
+
const [url] = mockFetch.mock.calls[0];
|
|
770
|
+
expect(url).toBe("https://app.dimensions.ai/api/test");
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("handles array params in GET request", async () => {
|
|
774
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
775
|
+
ok: true,
|
|
776
|
+
json: () => Promise.resolve({ data: "result" }),
|
|
777
|
+
});
|
|
778
|
+
globalThis.fetch = mockFetch;
|
|
779
|
+
|
|
780
|
+
await client.get("/api/test", {
|
|
781
|
+
params: { ids: [1, 2, 3] },
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const [url] = mockFetch.mock.calls[0];
|
|
785
|
+
expect(url).toBe("https://app.dimensions.ai/api/test?ids=1&ids=2&ids=3");
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("retries GET on server error", async () => {
|
|
789
|
+
const mockFetch = vi
|
|
790
|
+
.fn()
|
|
791
|
+
.mockResolvedValueOnce({
|
|
792
|
+
ok: false,
|
|
793
|
+
status: 500,
|
|
794
|
+
statusText: "Internal Server Error",
|
|
795
|
+
})
|
|
796
|
+
.mockResolvedValueOnce({
|
|
797
|
+
ok: true,
|
|
798
|
+
json: () => Promise.resolve({ data: "success" }),
|
|
799
|
+
});
|
|
800
|
+
globalThis.fetch = mockFetch;
|
|
801
|
+
|
|
802
|
+
const result = await client.get("/api/test");
|
|
803
|
+
|
|
804
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
805
|
+
expect(result).toEqual({ data: "success" });
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("supports custom headers in GET request", async () => {
|
|
809
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
810
|
+
ok: true,
|
|
811
|
+
json: () => Promise.resolve({ data: "result" }),
|
|
812
|
+
});
|
|
813
|
+
globalThis.fetch = mockFetch;
|
|
814
|
+
|
|
815
|
+
await client.get("/api/test", {
|
|
816
|
+
headers: { "X-Custom": "value" },
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
820
|
+
expect(options.headers["X-Custom"]).toBe("value");
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it("supports AbortSignal in GET request", async () => {
|
|
824
|
+
const controller = new AbortController();
|
|
825
|
+
controller.abort();
|
|
826
|
+
|
|
827
|
+
const mockFetch = vi.fn().mockImplementation(() => {
|
|
828
|
+
const error = new Error("Aborted");
|
|
829
|
+
error.name = "AbortError";
|
|
830
|
+
return Promise.reject(error);
|
|
831
|
+
});
|
|
832
|
+
globalThis.fetch = mockFetch;
|
|
833
|
+
|
|
834
|
+
const noRetryClient = new HttpClient({
|
|
835
|
+
baseUrl: "https://app.dimensions.ai",
|
|
836
|
+
authProvider: mockAuthProvider,
|
|
837
|
+
maxRetries: 0,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await expect(noRetryClient.get("/api/test", { signal: controller.signal })).rejects.toThrow(
|
|
841
|
+
TimeoutError,
|
|
842
|
+
);
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
describe("postJson method", () => {
|
|
847
|
+
it("sends POST request with JSON body", async () => {
|
|
848
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
849
|
+
ok: true,
|
|
850
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
851
|
+
});
|
|
852
|
+
globalThis.fetch = mockFetch;
|
|
853
|
+
|
|
854
|
+
const result = await client.postJson<{ result: string }>("/api/test", {
|
|
855
|
+
json: { query: "test", limit: 10 },
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
859
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
860
|
+
expect(url).toBe("https://app.dimensions.ai/api/test");
|
|
861
|
+
expect(options.method).toBe("POST");
|
|
862
|
+
expect(options.headers["Content-Type"]).toBe("application/json");
|
|
863
|
+
expect(options.body).toBe('{"query":"test","limit":10}');
|
|
864
|
+
expect(result).toEqual({ result: "success" });
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("retries postJson on rate limit error", async () => {
|
|
868
|
+
vi.spyOn(client as never, "delay" as never).mockResolvedValue(undefined as never);
|
|
869
|
+
|
|
870
|
+
const mockFetch = vi
|
|
871
|
+
.fn()
|
|
872
|
+
.mockResolvedValueOnce({
|
|
873
|
+
ok: false,
|
|
874
|
+
status: 429,
|
|
875
|
+
statusText: "Too Many Requests",
|
|
876
|
+
headers: new Headers({ "Retry-After": "1" }),
|
|
877
|
+
})
|
|
878
|
+
.mockResolvedValueOnce({
|
|
879
|
+
ok: true,
|
|
880
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
881
|
+
});
|
|
882
|
+
globalThis.fetch = mockFetch;
|
|
883
|
+
|
|
884
|
+
const result = await client.postJson("/api/test", { json: { data: 1 } });
|
|
885
|
+
|
|
886
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
887
|
+
expect(result).toEqual({ result: "success" });
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it("does NOT retry postJson on validation error", async () => {
|
|
891
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
892
|
+
ok: false,
|
|
893
|
+
status: 400,
|
|
894
|
+
statusText: "Bad Request",
|
|
895
|
+
json: () => Promise.resolve({ error: { message: "Invalid input" } }),
|
|
896
|
+
});
|
|
897
|
+
globalThis.fetch = mockFetch;
|
|
898
|
+
|
|
899
|
+
await expect(client.postJson("/api/test", { json: { invalid: true } })).rejects.toThrow(
|
|
900
|
+
QuerySyntaxError,
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("supports custom headers in postJson", async () => {
|
|
907
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
908
|
+
ok: true,
|
|
909
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
910
|
+
});
|
|
911
|
+
globalThis.fetch = mockFetch;
|
|
912
|
+
|
|
913
|
+
await client.postJson("/api/test", {
|
|
914
|
+
json: { data: 1 },
|
|
915
|
+
headers: { "X-API-Key": "secret" },
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
919
|
+
expect(options.headers["X-API-Key"]).toBe("secret");
|
|
920
|
+
expect(options.headers["Content-Type"]).toBe("application/json");
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
describe("postForm method", () => {
|
|
925
|
+
it("sends POST request with form-urlencoded body", async () => {
|
|
926
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
927
|
+
ok: true,
|
|
928
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
929
|
+
});
|
|
930
|
+
globalThis.fetch = mockFetch;
|
|
931
|
+
|
|
932
|
+
const result = await client.postForm<{ result: string }>("/api/test", {
|
|
933
|
+
form: { query: 'search publications for "test"' },
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
937
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
938
|
+
expect(url).toBe("https://app.dimensions.ai/api/test");
|
|
939
|
+
expect(options.method).toBe("POST");
|
|
940
|
+
expect(options.headers["Content-Type"]).toBe("application/x-www-form-urlencoded");
|
|
941
|
+
expect(options.body).toBe("query=search+publications+for+%22test%22");
|
|
942
|
+
expect(result).toEqual({ result: "success" });
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("encodes multiple form fields correctly", async () => {
|
|
946
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
947
|
+
ok: true,
|
|
948
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
949
|
+
});
|
|
950
|
+
globalThis.fetch = mockFetch;
|
|
951
|
+
|
|
952
|
+
await client.postForm("/api/test", {
|
|
953
|
+
form: { field1: "value1", field2: "value2" },
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
957
|
+
expect(options.body).toBe("field1=value1&field2=value2");
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("retries postForm on server error", async () => {
|
|
961
|
+
const mockFetch = vi
|
|
962
|
+
.fn()
|
|
963
|
+
.mockResolvedValueOnce({
|
|
964
|
+
ok: false,
|
|
965
|
+
status: 503,
|
|
966
|
+
statusText: "Service Unavailable",
|
|
967
|
+
})
|
|
968
|
+
.mockResolvedValueOnce({
|
|
969
|
+
ok: true,
|
|
970
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
971
|
+
});
|
|
972
|
+
globalThis.fetch = mockFetch;
|
|
973
|
+
|
|
974
|
+
const result = await client.postForm("/api/test", { form: { q: "test" } });
|
|
975
|
+
|
|
976
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
977
|
+
expect(result).toEqual({ result: "success" });
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("supports custom timeout in postForm", async () => {
|
|
981
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
982
|
+
ok: true,
|
|
983
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
984
|
+
});
|
|
985
|
+
globalThis.fetch = mockFetch;
|
|
986
|
+
|
|
987
|
+
await client.postForm("/api/test", {
|
|
988
|
+
form: { q: "test" },
|
|
989
|
+
timeout: 60000,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
describe("custom timeout in request options", () => {
|
|
997
|
+
it("uses custom timeout in GET request", async () => {
|
|
998
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
999
|
+
ok: true,
|
|
1000
|
+
json: () => Promise.resolve({ data: "result" }),
|
|
1001
|
+
});
|
|
1002
|
+
globalThis.fetch = mockFetch;
|
|
1003
|
+
|
|
1004
|
+
await client.get("/api/test", { timeout: 60000 });
|
|
1005
|
+
|
|
1006
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it("uses custom timeout in postJson request", async () => {
|
|
1010
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
1011
|
+
ok: true,
|
|
1012
|
+
json: () => Promise.resolve({ result: "success" }),
|
|
1013
|
+
});
|
|
1014
|
+
globalThis.fetch = mockFetch;
|
|
1015
|
+
|
|
1016
|
+
await client.postJson("/api/test", {
|
|
1017
|
+
json: { data: 1 },
|
|
1018
|
+
timeout: 60000,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
describe("rate limiting integration", () => {
|
|
1026
|
+
it("calls waitIfNeeded before request and recordRequest after success", async () => {
|
|
1027
|
+
const rateLimiter = new RateLimiter({ maxRequests: 10, windowMs: 60_000 });
|
|
1028
|
+
const waitSpy = vi.spyOn(rateLimiter, "waitIfNeeded");
|
|
1029
|
+
const recordSpy = vi.spyOn(rateLimiter, "recordRequest");
|
|
1030
|
+
|
|
1031
|
+
const rateLimitedClient = new HttpClient({
|
|
1032
|
+
baseUrl: "https://app.dimensions.ai",
|
|
1033
|
+
authProvider: mockAuthProvider,
|
|
1034
|
+
rateLimiter,
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
1038
|
+
ok: true,
|
|
1039
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
1040
|
+
});
|
|
1041
|
+
globalThis.fetch = mockFetch;
|
|
1042
|
+
|
|
1043
|
+
await rateLimitedClient.query("search publications");
|
|
1044
|
+
|
|
1045
|
+
expect(waitSpy).toHaveBeenCalledTimes(1);
|
|
1046
|
+
expect(recordSpy).toHaveBeenCalledTimes(1);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it("does not record request on failure", async () => {
|
|
1050
|
+
const rateLimiter = new RateLimiter({ maxRequests: 10, windowMs: 60_000 });
|
|
1051
|
+
const recordSpy = vi.spyOn(rateLimiter, "recordRequest");
|
|
1052
|
+
|
|
1053
|
+
const rateLimitedClient = new HttpClient({
|
|
1054
|
+
baseUrl: "https://app.dimensions.ai",
|
|
1055
|
+
authProvider: mockAuthProvider,
|
|
1056
|
+
maxRetries: 0,
|
|
1057
|
+
rateLimiter,
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
1061
|
+
ok: false,
|
|
1062
|
+
status: 400,
|
|
1063
|
+
statusText: "Bad Request",
|
|
1064
|
+
json: () => Promise.resolve({ error: { message: "Syntax error" } }),
|
|
1065
|
+
});
|
|
1066
|
+
globalThis.fetch = mockFetch;
|
|
1067
|
+
|
|
1068
|
+
await rateLimitedClient.query("bad query").catch(() => {});
|
|
1069
|
+
|
|
1070
|
+
expect(recordSpy).not.toHaveBeenCalled();
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it("calls waitIfNeeded before each retry attempt", async () => {
|
|
1074
|
+
const rateLimiter = new RateLimiter({ maxRequests: 10, windowMs: 60_000 });
|
|
1075
|
+
const waitSpy = vi.spyOn(rateLimiter, "waitIfNeeded");
|
|
1076
|
+
const recordSpy = vi.spyOn(rateLimiter, "recordRequest");
|
|
1077
|
+
|
|
1078
|
+
const rateLimitedClient = new HttpClient({
|
|
1079
|
+
baseUrl: "https://app.dimensions.ai",
|
|
1080
|
+
authProvider: mockAuthProvider,
|
|
1081
|
+
maxRetries: 2,
|
|
1082
|
+
retryDelay: 10,
|
|
1083
|
+
rateLimiter,
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const mockFetch = vi
|
|
1087
|
+
.fn()
|
|
1088
|
+
.mockResolvedValueOnce({
|
|
1089
|
+
ok: false,
|
|
1090
|
+
status: 500,
|
|
1091
|
+
statusText: "Internal Server Error",
|
|
1092
|
+
})
|
|
1093
|
+
.mockResolvedValueOnce({
|
|
1094
|
+
ok: true,
|
|
1095
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
1096
|
+
});
|
|
1097
|
+
globalThis.fetch = mockFetch;
|
|
1098
|
+
|
|
1099
|
+
await rateLimitedClient.query("search publications");
|
|
1100
|
+
|
|
1101
|
+
// waitIfNeeded called before initial attempt and before retry
|
|
1102
|
+
expect(waitSpy).toHaveBeenCalledTimes(2);
|
|
1103
|
+
// recordRequest called only on final success
|
|
1104
|
+
expect(recordSpy).toHaveBeenCalledTimes(1);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it("works without rate limiter (backward compatibility)", async () => {
|
|
1108
|
+
const clientWithoutLimiter = new HttpClient({
|
|
1109
|
+
baseUrl: "https://app.dimensions.ai",
|
|
1110
|
+
authProvider: mockAuthProvider,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
1114
|
+
ok: true,
|
|
1115
|
+
json: () => Promise.resolve({ _stats: { total_count: 0 } }),
|
|
1116
|
+
});
|
|
1117
|
+
globalThis.fetch = mockFetch;
|
|
1118
|
+
|
|
1119
|
+
const result = await clientWithoutLimiter.query("search publications");
|
|
1120
|
+
|
|
1121
|
+
expect(result).toEqual({ _stats: { total_count: 0 } });
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it("records request correctly across all HTTP methods", async () => {
|
|
1125
|
+
const rateLimiter = new RateLimiter({ maxRequests: 10, windowMs: 60_000 });
|
|
1126
|
+
const recordSpy = vi.spyOn(rateLimiter, "recordRequest");
|
|
1127
|
+
|
|
1128
|
+
const rateLimitedClient = new HttpClient({
|
|
1129
|
+
baseUrl: "https://app.dimensions.ai",
|
|
1130
|
+
authProvider: mockAuthProvider,
|
|
1131
|
+
rateLimiter,
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
1135
|
+
ok: true,
|
|
1136
|
+
json: () => Promise.resolve({ data: "ok" }),
|
|
1137
|
+
});
|
|
1138
|
+
globalThis.fetch = mockFetch;
|
|
1139
|
+
|
|
1140
|
+
await rateLimitedClient.query("search publications");
|
|
1141
|
+
await rateLimitedClient.get("/api/test");
|
|
1142
|
+
await rateLimitedClient.postJson("/api/test", { json: {} });
|
|
1143
|
+
await rateLimitedClient.postForm("/api/test", { form: { q: "test" } });
|
|
1144
|
+
|
|
1145
|
+
expect(recordSpy).toHaveBeenCalledTimes(4);
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
});
|