From 19d158b358152204f01bddd0af590e82ea5dee06 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 10 Jun 2025 13:08:28 +0300 Subject: [PATCH 1/8] import POC --- examples/lua-multi-incr.js | 1 + packages/client/lib/cluster/index.ts | 9 + .../command-router.ts | 15 + .../dynamic-policy-resolver-factory.ts | 109 ++++++ .../dynamic-policy-resolver.spec.ts | 317 ++++++++++++++++++ .../request-response-policies/index.ts | 10 + .../policies-constants.ts | 30 ++ .../static-policies-data.ts | 59 ++++ .../static-policy-resolver.ts | 61 ++++ .../request-response-policies/test.spec.ts | 9 + .../request-response-policies/types.ts | 22 ++ .../lib/commands/generic-transformers.ts | 12 +- 12 files changed, 650 insertions(+), 4 deletions(-) create mode 100644 packages/client/lib/cluster/request-response-policies/command-router.ts create mode 100644 packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts create mode 100644 packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts create mode 100644 packages/client/lib/cluster/request-response-policies/index.ts create mode 100644 packages/client/lib/cluster/request-response-policies/policies-constants.ts create mode 100644 packages/client/lib/cluster/request-response-policies/static-policies-data.ts create mode 100644 packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts create mode 100644 packages/client/lib/cluster/request-response-policies/test.spec.ts create mode 100644 packages/client/lib/cluster/request-response-policies/types.ts diff --git a/examples/lua-multi-incr.js b/examples/lua-multi-incr.js index 71b12bdab0..a5ad558b0c 100644 --- a/examples/lua-multi-incr.js +++ b/examples/lua-multi-incr.js @@ -7,6 +7,7 @@ const client = createClient({ scripts: { mincr: defineScript({ NUMBER_OF_KEYS: 2, + // TODO add RequestPolicy: , SCRIPT: 'return {' + 'redis.pcall("INCRBY", KEYS[1], ARGV[1]),' + diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index c2c251810e..b5b9879597 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -13,6 +13,7 @@ import { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/ import { BasicCommandParser } from '../client/parser'; import { ASKING_CMD } from '../commands/ASKING'; import SingleEntryCache from '../single-entry-cache' +import { POLICIES, PolicyResolver, StaticPolicyResolver } from './request-response-policies'; interface ClusterCommander< M extends RedisModules, F extends RedisFunctions, @@ -187,6 +188,7 @@ export default class RedisCluster< return async function (this: ProxyCluster, ...args: Array) { const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); + console.log(parser, parser.redisArgs[0]); return this._self._execute( parser.firstKey, @@ -299,6 +301,7 @@ export default class RedisCluster< private _self = this; private _commandOptions?: ClusterCommandOptions; + private _policyResolver: PolicyResolver; /** * An array of the cluster slots, each slot contain its `master` and `replicas`. @@ -356,6 +359,8 @@ export default class RedisCluster< if (options?.commandOptions) { this._commandOptions = options.commandOptions; } + + this._policyResolver = new StaticPolicyResolver(POLICIES); } duplicate< @@ -456,7 +461,11 @@ export default class RedisCluster< options: ClusterCommandOptions | undefined, fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { + console.log(`executing command `, firstKey, isReadonly, options); const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; + const p = this._policyResolver.resolvePolicy("ping") + console.log(`ping policy `, p); + let client = await this._slots.getClient(firstKey, isReadonly); let i = 0; diff --git a/packages/client/lib/cluster/request-response-policies/command-router.ts b/packages/client/lib/cluster/request-response-policies/command-router.ts new file mode 100644 index 0000000000..e7dacb51f8 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/command-router.ts @@ -0,0 +1,15 @@ +// import { RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from "../../RESP/types"; +// import { ShardNode } from "../cluster-slots"; +// import type { Either } from './types'; + +// export interface CommandRouter< +// M extends RedisModules, +// F extends RedisFunctions, +// S extends RedisScripts, +// RESP extends RespVersions, +// TYPE_MAPPING extends TypeMapping> { +// routeCommand( +// command: string, +// policy: RequestPolicy, +// ): Either, 'no-available-nodes' | 'routing-failed'>; +// } \ No newline at end of file diff --git a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts new file mode 100644 index 0000000000..1712390a80 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts @@ -0,0 +1,109 @@ +import type { CommandReply } from '../../commands/generic-transformers'; +import type { CommandPolicies } from './policies-constants'; +import { REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS } from './policies-constants'; +import type { PolicyResolver } from './types'; +import { StaticPolicyResolver } from './static-policy-resolver'; +import type { ModulePolicyRecords } from './static-policies-data'; + +/** + * Function type that returns command information from Redis + */ +export type CommandFetcher = () => Promise>; + +/** + * A factory for creating policy resolvers that dynamically build policies based on the Redis server's COMMAND response. + * + * This factory fetches command information from Redis and analyzes the response to determine + * appropriate routing policies for each command, returning a StaticPolicyResolver with the built policies. + */ +export class DynamicPolicyResolverFactory { + /** + * Creates a StaticPolicyResolver by fetching command information from Redis + * and building appropriate policies based on the command characteristics. + * + * @param commandFetcher Function to fetch command information from Redis + * @param fallbackResolver Optional fallback resolver to use when policies are not found + * @returns A new StaticPolicyResolver with the fetched policies + */ + static async create( + commandFetcher: CommandFetcher, + fallbackResolver?: PolicyResolver + ): Promise { + const commands = await commandFetcher(); + const policies: ModulePolicyRecords = {}; + + for (const command of commands) { + const parsed = DynamicPolicyResolverFactory.#parseCommandName(command.name); + + // Skip commands with invalid format (more than one dot) + if (!parsed) { + continue; + } + + const { moduleName, commandName } = parsed; + + // Initialize module if it doesn't exist + if (!policies[moduleName]) { + policies[moduleName] = {}; + } + + // Determine policies for this command + const commandPolicies = DynamicPolicyResolverFactory.#buildCommandPolicies(command); + policies[moduleName][commandName] = commandPolicies; + } + + return new StaticPolicyResolver(policies, fallbackResolver); + } + + /** + * Parses a command name to extract module and command components. + * + * Redis commands can be in format: + * - "ping" -> module: "std", command: "ping" + * - "ft.search" -> module: "ft", command: "search" + * + * Commands with more than one dot are invalid. + */ + static #parseCommandName(fullCommandName: string): { moduleName: string; commandName: string } | null { + const parts = fullCommandName.split('.'); + + if (parts.length === 1) { + return { moduleName: 'std', commandName: fullCommandName }; + } + + if (parts.length === 2) { + return { moduleName: parts[0], commandName: parts[1] }; + } + + // Commands with more than one dot are invalid in Redis + return null; + } + + /** + * Builds CommandPolicies for a command based on its characteristics. + * + * Priority order: + * 1. Use explicit policies from the command if available + * 2. Classify as DEFAULT_KEYLESS if keySpecification is empty + * 3. Classify as DEFAULT_KEYED if keySpecification is not empty + */ + static #buildCommandPolicies(command: CommandReply): CommandPolicies { + // Determine if command is keyless based on keySpecification + const isKeyless = command.keySpecifications === 'keyless'; + + // Determine default policies based on key specification + const defaultRequest = isKeyless + ? REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS + : REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED; + const defaultResponse = isKeyless + ? RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS + : RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED; + + return { + // request: command.policies.request ?? defaultRequest, + // response: command.policies.response ?? defaultResponse + request: defaultRequest, + response: defaultResponse + }; + } +} \ No newline at end of file diff --git a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts new file mode 100644 index 0000000000..fe115addb3 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts @@ -0,0 +1,317 @@ +import { strict as assert } from 'node:assert'; +import type { CommandReply } from '../../commands/generic-transformers'; +import { DynamicPolicyResolverFactory, type CommandFetcher, StaticPolicyResolver, REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS } from '.'; +import testUtils, { GLOBAL } from '../../test-utils'; + + +const createMockCommandFetcher = (commands: Array): CommandFetcher => async () => commands; + +describe('DynamicPolicyResolverFactory', () => { + + describe('create', () => { + it('should create StaticPolicyResolver with empty policies', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + assert.ok(resolver instanceof StaticPolicyResolver); + }); + + it('should create StaticPolicyResolver with fallback', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const fallbackResolver = new StaticPolicyResolver({ + std: { + ping: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS + } + } + }); + + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher, fallbackResolver); + assert.ok(resolver instanceof StaticPolicyResolver); + + const result = resolver.resolvePolicy('ping'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + }); + + describe('create with commands', () => { + it('should classify keyless commands correctly', async () => { + const mockCommands: Array = [ + { + name: 'ping', + arity: -1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + // policies: { request: undefined, response: undefined }, + keySpecifications: 'keyless' + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('ping'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + + it('should classify keyed commands correctly', async () => { + const mockCommands: Array = [ + { + name: 'get', + arity: 2, + flags: new Set(), + firstKeyIndex: 1, + lastKeyIndex: 1, + step: 1, + categories: new Set(), + // policies: { request: undefined, response: undefined }, + keySpecifications: [Buffer.from('key')] as any + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('get'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + } + }); + + it('should use explicit policies when available', async () => { + const mockCommands: Array = [ + { + name: 'dbsize', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + // policies: { request: 'all_shards', response: 'agg_sum' }, + keySpecifications: 'keyless' + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('dbsize'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, 'all_shards'); + assert.equal(result.value.response, 'agg_sum'); + } + }); + + it('should handle module commands correctly', async () => { + const mockCommands: Array = [ + { + name: 'ft.search', + arity: -2, + flags: new Set(), + firstKeyIndex: 1, + lastKeyIndex: 1, + step: 1, + categories: new Set(), + // policies: { request: 'all_shards', response: 'special' }, + keySpecifications: [Buffer.from('key')] as any + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('ft.search'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, 'all_shards'); + assert.equal(result.value.response, 'special'); + } + }); + + it('should handle valid module commands', async () => { + const mockCommands: Array = [ + { + name: 'json.get', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + // policies: { request: undefined, response: undefined }, + keySpecifications: 'keyless' + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('json.get'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + }); + + describe('resolvePolicy', () => { + it('should work with created resolver', async () => { + const mockCommands: Array = [ + { + name: 'test', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + // policies: { request: undefined, response: undefined }, + keySpecifications: 'keyless' + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('test'); + assert.equal(result.ok, true); + }); + + it('should handle unknown commands', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('unknown'); + assert.equal(result.ok, false); + assert.equal(result.error, 'unknown-command'); + }); + + it('should handle unknown modules', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('unknown.command'); + assert.equal(result.ok, false); + assert.equal(result.error, 'unknown-module'); + }); + + it('should handle invalid command format', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy('too.many.dots.here'); + assert.equal(result.ok, false); + assert.equal(result.error, 'wrong-command-or-module-name'); + }); + }); + + describe('edge cases', () => { + it('should handle commands with partial policies', async () => { + const mockCommands: Array = [ + { + name: 'partial-request', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + // policies: { request: 'all_nodes', response: undefined }, + keySpecifications: [Buffer.from('key')] as any + }, + { + name: 'partial-response', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + // policies: { request: undefined, response: 'agg_sum' }, + keySpecifications: 'keyless' + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + // Command with only request policy should fall back to defaults + let result = resolver.resolvePolicy('partial-request'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.ALL_NODES); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + } + + // Command with only response policy should fall back to defaults + result = resolver.resolvePolicy('partial-response'); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.AGG_SUM); + } + }); + + it('should handle empty command list', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + assert.ok(resolver instanceof StaticPolicyResolver); + + const result = resolver.resolvePolicy('any-command'); + assert.equal(result.ok, false); + assert.equal(result.error, 'unknown-command'); + }); + }); + + describe('integration tests', () => { + testUtils.testWithClient('should work with real Redis client', async client => { + const resolver = await DynamicPolicyResolverFactory.create(() => client.command()); + assert.ok(resolver instanceof StaticPolicyResolver); + + // Test that ping command is classified as keyless + const pingResult = resolver.resolvePolicy('ping'); + if (pingResult.ok) { + assert.equal(pingResult.value.request, REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS); + assert.equal(pingResult.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED); + } else { + assert.fail('Expected pingResult.ok to be true'); + } + + // Test that get command is classified as keyed + const getResult = resolver.resolvePolicy('get'); + if (getResult.ok) { + assert.equal(getResult.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(getResult.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + } else { + assert.fail('Expected getResult.ok to be true'); + } + + // Test that dbsize command uses explicit policies if available + const dbsizeResult = resolver.resolvePolicy('dbsize'); + + if (dbsizeResult.ok) { + assert.ok( + dbsizeResult.value.request === 'all_shards' && dbsizeResult.value.response === 'agg_sum' + ); + } else { + assert.fail('Expected dbsizeResult.ok to be true'); + } + }, GLOBAL.SERVERS.OPEN); + }); +}); diff --git a/packages/client/lib/cluster/request-response-policies/index.ts b/packages/client/lib/cluster/request-response-policies/index.ts new file mode 100644 index 0000000000..24a1761451 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/index.ts @@ -0,0 +1,10 @@ +export type { Either, PolicyResult, PolicyResolver } from './types'; + +export { StaticPolicyResolver } from './static-policy-resolver'; +export { DynamicPolicyResolverFactory, type CommandFetcher } from './dynamic-policy-resolver-factory'; + +export * from './policies-constants'; +export type { ModulePolicyRecords, CommandPolicyRecords } from './static-policies-data'; +export { POLICIES } from './static-policies-data'; + +// export { type CommandRouter } from './command-router'; \ No newline at end of file diff --git a/packages/client/lib/cluster/request-response-policies/policies-constants.ts b/packages/client/lib/cluster/request-response-policies/policies-constants.ts new file mode 100644 index 0000000000..d27c550d75 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/policies-constants.ts @@ -0,0 +1,30 @@ +export const REQUEST_POLICIES_WITH_DEFAULTS = { + ALL_NODES: "all_nodes", + ALL_SHARDS: "all_shards", + MULTI_SHARD: "multi_shard", + SPECIAL: "special", + DEFAULT_KEYLESS: "default-keyless", + DEFAULT_KEYED: "default-keyed" +} as const; + +export type RequestPolicyWithDefaults = typeof REQUEST_POLICIES_WITH_DEFAULTS[keyof typeof REQUEST_POLICIES_WITH_DEFAULTS]; + +export const RESPONSE_POLICIES_WITH_DEFAULTS = { + ONE_SUCCEEDED: "one_succeeded", + ALL_SUCCEEDED: "all_succeeded", + AGG_LOGICAL_AND: "agg_logical_and", + AGG_LOGICAL_OR: "agg_logical_or", + AGG_MIN: "agg_min", + AGG_MAX: "agg_max", + AGG_SUM: "agg_sum", + SPECIAL: "special", + DEFAULT_KEYLESS: "default-keyless", + DEFAULT_KEYED: "default-keyed" +} as const; + +export type ResponsePolicyWithDefaults = typeof RESPONSE_POLICIES_WITH_DEFAULTS[keyof typeof RESPONSE_POLICIES_WITH_DEFAULTS]; + +export interface CommandPolicies { + readonly request: RequestPolicyWithDefaults; + readonly response: ResponsePolicyWithDefaults; +} \ No newline at end of file diff --git a/packages/client/lib/cluster/request-response-policies/static-policies-data.ts b/packages/client/lib/cluster/request-response-policies/static-policies-data.ts new file mode 100644 index 0000000000..68c44c3129 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/static-policies-data.ts @@ -0,0 +1,59 @@ +import { CommandPolicies, REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS } from './policies-constants'; + +export type CommandPolicyRecords = Record; +// The response of the COMMAND command uses "." to separate the module name from the command name. +// For example, "ft.search" refers to the "search" command in the "ft" module. It is important to use the same naming convention here. +export type ModulePolicyRecords = Record; + +export const POLICIES: ModulePolicyRecords = { + ft: { + create: { + request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_NODES, + response: RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED + }, + search: { + request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL + }, + aggregate: { + request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL + }, + sugadd: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED + }, + sugget: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED + }, + sugdel: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED + }, + suglen: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED + }, + spellcheck: { + request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL + }, + cursor: { + request: REQUEST_POLICIES_WITH_DEFAULTS.SPECIAL, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS + }, + dictadd: { + request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED + }, + dictdel: { + request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED + }, + dictdump: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS + } + } +} as const; diff --git a/packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts b/packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts new file mode 100644 index 0000000000..2d9472fd95 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts @@ -0,0 +1,61 @@ +import type { PolicyResult, PolicyResolver } from './types'; +import { POLICIES } from './static-policies-data'; + +export class StaticPolicyResolver implements PolicyResolver { + private readonly fallbackResolver: PolicyResolver | null = null; + + constructor( + private readonly policies = POLICIES, + fallbackResolver?: PolicyResolver + ) { + this.fallbackResolver = fallbackResolver || null; + } + + /** + * Sets a fallback resolver to use when policies are not found in this resolver. + * + * @param fallbackResolver The resolver to fall back to + * @returns A new StaticPolicyResolver with the specified fallback + */ + withFallback(fallbackResolver: PolicyResolver): StaticPolicyResolver { + return new StaticPolicyResolver(this.policies, fallbackResolver); + } + + resolvePolicy(command: string): PolicyResult { + const parts = command.split('.'); + + if (parts.length > 2) { + return { ok: false, error: 'wrong-command-or-module-name' }; + } + + const [moduleName, commandName] = parts.length === 1 + ? ['std', command] + : parts; + + if (!this.policies[moduleName]) { + if (this.fallbackResolver) { + return this.fallbackResolver.resolvePolicy(command); + } + + // For std module commands, return 'unknown-command' instead of 'unknown-module' + // to provide better UX for single-word commands + if (moduleName === 'std') { + return { ok: false, error: 'unknown-command' }; + } + return { ok: false, error: 'unknown-module' }; + } + + if (!this.policies[moduleName][commandName]) { + // Try fallback resolver if available + if (this.fallbackResolver) { + return this.fallbackResolver.resolvePolicy(command); + } + return { ok: false, error: 'unknown-command' }; + } + + return { + ok: true, + value: this.policies[moduleName][commandName] + } + } +} diff --git a/packages/client/lib/cluster/request-response-policies/test.spec.ts b/packages/client/lib/cluster/request-response-policies/test.spec.ts new file mode 100644 index 0000000000..837a837d74 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/test.spec.ts @@ -0,0 +1,9 @@ +import testUtils, { GLOBAL } from '../../test-utils'; + +describe('Cluster Request-Response Policies', () => { + testUtils.testWithCluster('should resolve policies correctly', async cluster => { + + await cluster.get('foo') + + }, GLOBAL.CLUSTERS.OPEN); +}); diff --git a/packages/client/lib/cluster/request-response-policies/types.ts b/packages/client/lib/cluster/request-response-policies/types.ts new file mode 100644 index 0000000000..27d2cae8a7 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/types.ts @@ -0,0 +1,22 @@ +import type { CommandPolicies } from './policies-constants'; + +export type Either = + | { readonly ok: true; readonly value: TOk } + | { readonly ok: false; readonly error: TError }; + +export type PolicyResult = Either; + +export interface PolicyResolver { + /** + * The response of the COMMAND command uses "." to separate the module name from the command name. + */ + resolvePolicy(command: string): PolicyResult; + + /** + * Sets a fallback resolver to use when policies are not found in this resolver. + * + * @param fallbackResolver The resolver to fall back to + * @returns A new PolicyResolver with the specified fallback + */ + withFallback(fallbackResolver: PolicyResolver): PolicyResolver; +} diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index 91eab7107a..d0b713b7b9 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -329,7 +329,9 @@ export type CommandRawReply = [ firstKeyIndex: number, lastKeyIndex: number, step: number, - categories: Array + categories: Array, + tips: Array, + keySpecifications: string ]; export type CommandReply = { @@ -339,12 +341,13 @@ export type CommandReply = { firstKeyIndex: number, lastKeyIndex: number, step: number, - categories: Set + categories: Set, + keySpecifications: string }; export function transformCommandReply( this: void, - [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories]: CommandRawReply + [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories, _tips, keySpecifications]: CommandRawReply ): CommandReply { return { name, @@ -353,7 +356,8 @@ export function transformCommandReply( firstKeyIndex, lastKeyIndex, step, - categories: new Set(categories) + categories: new Set(categories), + keySpecifications }; } From 99e26a1d55e5cbe0e8ce3648a439a5266067b2c0 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 10 Jun 2025 17:46:25 +0300 Subject: [PATCH 2/8] use lowercase for command matching --- .../static-policy-resolver.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts b/packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts index 2d9472fd95..9d7772d40f 100644 --- a/packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts +++ b/packages/client/lib/cluster/request-response-policies/static-policy-resolver.ts @@ -22,19 +22,22 @@ export class StaticPolicyResolver implements PolicyResolver { } resolvePolicy(command: string): PolicyResult { - const parts = command.split('.'); + const parts = command.toLowerCase().split('.'); + if (parts.length > 2) { return { ok: false, error: 'wrong-command-or-module-name' }; } const [moduleName, commandName] = parts.length === 1 - ? ['std', command] + ? ['std', parts[0]] : parts; + console.log(`module name `, moduleName, `command name `, commandName); + if (!this.policies[moduleName]) { if (this.fallbackResolver) { - return this.fallbackResolver.resolvePolicy(command); + return this.fallbackResolver.resolvePolicy(commandName); } // For std module commands, return 'unknown-command' instead of 'unknown-module' @@ -48,7 +51,7 @@ export class StaticPolicyResolver implements PolicyResolver { if (!this.policies[moduleName][commandName]) { // Try fallback resolver if available if (this.fallbackResolver) { - return this.fallbackResolver.resolvePolicy(command); + return this.fallbackResolver.resolvePolicy(commandName); } return { ok: false, error: 'unknown-command' }; } From 0ceebda7ca753ec3168fac50dff4dda3e243f21e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 10 Jun 2025 17:46:39 +0300 Subject: [PATCH 3/8] expose commandName getter --- packages/client/lib/client/parser.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/client/lib/client/parser.ts b/packages/client/lib/client/parser.ts index 3e82023042..47689ae67e 100644 --- a/packages/client/lib/client/parser.ts +++ b/packages/client/lib/client/parser.ts @@ -44,6 +44,14 @@ export class BasicCommandParser implements CommandParser { return tmp.join('_'); } + get commandName(): string | undefined { + let cmdName = this.#redisArgs[0]; + if (cmdName instanceof Buffer) { + return cmdName.toString(); + } + return cmdName; + } + push(...arg: Array) { this.#redisArgs.push(...arg); }; From 5cd7a38f81f287dee9ec398e7254444434521cb4 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Tue, 10 Jun 2025 17:48:00 +0300 Subject: [PATCH 4/8] pass down command name and search policy --- packages/client/lib/cluster/index.ts | 16 ++++++--- .../request-response-policies/test.spec.ts | 35 +++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index b5b9879597..3ce50c027d 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -188,12 +188,12 @@ export default class RedisCluster< return async function (this: ProxyCluster, ...args: Array) { const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - console.log(parser, parser.redisArgs[0]); return this._self._execute( parser.firstKey, command.IS_READ_ONLY, this._commandOptions, + parser.commandName!, (client, opts) => client._executeCommand(command, parser, opts, transformReply) ); }; @@ -210,6 +210,7 @@ export default class RedisCluster< parser.firstKey, command.IS_READ_ONLY, this._self._commandOptions, + parser.commandName!, (client, opts) => client._executeCommand(command, parser, opts, transformReply) ); }; @@ -228,6 +229,7 @@ export default class RedisCluster< parser.firstKey, fn.IS_READ_ONLY, this._self._commandOptions, + parser.commandName!, (client, opts) => client._executeCommand(fn, parser, opts, transformReply) ); }; @@ -246,6 +248,7 @@ export default class RedisCluster< parser.firstKey, script.IS_READ_ONLY, this._commandOptions, + parser.commandName!, (client, opts) => client._executeScript(script, parser, opts, transformReply) ); }; @@ -459,12 +462,16 @@ export default class RedisCluster< firstKey: RedisArgument | undefined, isReadonly: boolean | undefined, options: ClusterCommandOptions | undefined, + commandName: string, fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { - console.log(`executing command `, firstKey, isReadonly, options); const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; - const p = this._policyResolver.resolvePolicy("ping") - console.log(`ping policy `, p); + const policyResult = this._policyResolver.resolvePolicy(commandName) + if(policyResult.ok) { + //TODO + } else { + //TODO + } let client = await this._slots.getClient(firstKey, isReadonly); let i = 0; @@ -521,6 +528,7 @@ export default class RedisCluster< firstKey, isReadonly, options, + args[0] instanceof Buffer ? args[0].toString() : args[0], (client, opts) => client.sendCommand(args, opts) ); } diff --git a/packages/client/lib/cluster/request-response-policies/test.spec.ts b/packages/client/lib/cluster/request-response-policies/test.spec.ts index 837a837d74..2e3f15c389 100644 --- a/packages/client/lib/cluster/request-response-policies/test.spec.ts +++ b/packages/client/lib/cluster/request-response-policies/test.spec.ts @@ -1,9 +1,38 @@ import testUtils, { GLOBAL } from '../../test-utils'; +import RediSearch from '@redis/search'; + +import RedisBloomModules from '@redis/bloom'; +import RedisJSON from '@redis/json'; +import RedisTimeSeries from '@redis/time-series'; describe('Cluster Request-Response Policies', () => { - testUtils.testWithCluster('should resolve policies correctly', async cluster => { + testUtils.testWithClient('should resolve policies correctly', async client => { + await client.ft.SUGADD('index', 'string', 1); + await client.ft.dictAdd('index', 'foo'); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + modules: { + ft: RediSearch + } + } + }); - await cluster.get('foo') + testUtils.testWithCluster('should resolve policies correctly', async cluster => { - }, GLOBAL.CLUSTERS.OPEN); + await cluster.ft.SUGADD('index', 'string', 1); + await cluster.ft.DICTADD('index', 'foo'); + await cluster.sendCommand(undefined, true, ['ft.dictadd', 'index', 'string']); + + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + modules: { + ft: RediSearch, + // ...RedisBloomModules, + // json: RedisJSON, + // ts: RedisTimeSeries + }, + } + }); }); From a32a0dd1b4a4f9257afec626017c321fac7ef0b9 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 11 Jun 2025 11:52:49 +0300 Subject: [PATCH 5/8] partially parse tips and key specs [skip ci] for now we only extract: - request and response policy -> from tips - is the command keyless -> from key specs --- .../dynamic-policy-resolver-factory.ts | 2 +- .../dynamic-policy-resolver.spec.ts | 32 ++-- packages/client/lib/commands/COMMAND.spec.ts | 138 ++++++++++++++++-- .../lib/commands/generic-transformers.ts | 26 +++- 4 files changed, 162 insertions(+), 36 deletions(-) diff --git a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts index 1712390a80..03d26c9859 100644 --- a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts +++ b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts @@ -89,7 +89,7 @@ export class DynamicPolicyResolverFactory { */ static #buildCommandPolicies(command: CommandReply): CommandPolicies { // Determine if command is keyless based on keySpecification - const isKeyless = command.keySpecifications === 'keyless'; + const isKeyless = command.isKeyless // Determine default policies based on key specification const defaultRequest = isKeyless diff --git a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts index fe115addb3..f1a4feb16b 100644 --- a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts +++ b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts @@ -49,8 +49,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 0, step: 0, categories: new Set(), - // policies: { request: undefined, response: undefined }, - keySpecifications: 'keyless' + policies: { request: undefined, response: undefined }, + isKeyless: true } ]; @@ -75,8 +75,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 1, step: 1, categories: new Set(), - // policies: { request: undefined, response: undefined }, - keySpecifications: [Buffer.from('key')] as any + policies: { request: undefined, response: undefined }, + isKeyless: false } ]; @@ -101,8 +101,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 0, step: 0, categories: new Set(), - // policies: { request: 'all_shards', response: 'agg_sum' }, - keySpecifications: 'keyless' + policies: { request: 'all_shards', response: 'agg_sum' }, + isKeyless: true } ]; @@ -127,8 +127,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 1, step: 1, categories: new Set(), - // policies: { request: 'all_shards', response: 'special' }, - keySpecifications: [Buffer.from('key')] as any + policies: { request: 'all_shards', response: 'special' }, + isKeyless: false } ]; @@ -153,8 +153,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 0, step: 0, categories: new Set(), - // policies: { request: undefined, response: undefined }, - keySpecifications: 'keyless' + policies: { request: undefined, response: undefined }, + isKeyless: true } ]; @@ -181,8 +181,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 0, step: 0, categories: new Set(), - // policies: { request: undefined, response: undefined }, - keySpecifications: 'keyless' + policies: { request: undefined, response: undefined }, + isKeyless: true } ]; @@ -232,8 +232,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 0, step: 0, categories: new Set(), - // policies: { request: 'all_nodes', response: undefined }, - keySpecifications: [Buffer.from('key')] as any + policies: { request: 'all_nodes', response: undefined }, + isKeyless: false }, { name: 'partial-response', @@ -243,8 +243,8 @@ describe('DynamicPolicyResolverFactory', () => { lastKeyIndex: 0, step: 0, categories: new Set(), - // policies: { request: undefined, response: 'agg_sum' }, - keySpecifications: 'keyless' + policies: { request: undefined, response: 'agg_sum' }, + isKeyless: true } ]; diff --git a/packages/client/lib/commands/COMMAND.spec.ts b/packages/client/lib/commands/COMMAND.spec.ts index 860ffc3068..8a3900d9d6 100644 --- a/packages/client/lib/commands/COMMAND.spec.ts +++ b/packages/client/lib/commands/COMMAND.spec.ts @@ -1,17 +1,125 @@ -// import { strict as assert } from 'node:assert'; -// import testUtils, { GLOBAL } from '../test-utils'; -// import { transformArguments } from './COMMAND'; -// import { assertPingCommand } from './COMMAND_INFO.spec'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs, transformCommandReply, CommandFlags, CommandCategories, CommandRawReply } from './generic-transformers'; +import COMMAND from './COMMAND'; -// describe('COMMAND', () => { -// it('transformArguments', () => { -// assert.deepEqual( -// transformArguments(), -// ['COMMAND'] -// ); -// }); +describe('COMMAND', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(COMMAND), + ['COMMAND'] + ); + }); -// testUtils.testWithClient('client.command', async client => { -// assertPingCommand((await client.command()).find(command => command.name === 'ping')); -// }, GLOBAL.SERVERS.OPEN); -// }); + describe('transformCommandReply', () => { + const testCases = [ + { + name: 'without policies', + input: ['ping', -1, [CommandFlags.STALE], 0, 0, 0, [CommandCategories.FAST], [], []] satisfies CommandRawReply, + expected: { + name: 'ping', + arity: -1, + flags: new Set([CommandFlags.STALE]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([CommandCategories.FAST]), + policies: { request: undefined, response: undefined }, + isKeyless: true + } + }, + { + name: 'with valid policies', + input: ['dbsize', 1, [], 0, 0, 0, [], ['request_policy:all_shards', 'response_policy:agg_sum'], []] satisfies CommandRawReply, + expected: { + name: 'dbsize', + arity: 1, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: 'all_shards', response: 'agg_sum' }, + isKeyless: true + } + }, + { + name: 'with invalid policies', + input: ['test', 0, [], 0, 0, 0, [], ['request_policy:invalid', 'response_policy:invalid'], ['some key specification']] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: undefined, response: undefined }, + isKeyless: false + } + }, + { + name: 'with request policy only', + input: ['test', 0, [], 0, 0, 0, [], ['request_policy:all_nodes'], ['some key specification']] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: 'all_nodes', response: undefined }, + isKeyless: false + } + }, + { + name: 'with response policy only', + input: ['test', 0, [], 0, 0, 0, [], ['', 'response_policy:agg_max'], []] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: undefined, response: 'agg_max' }, + isKeyless: true + } + }, + { + name: 'with response policy only', + input: ['test', 0, [], 0, 0, 0, [], ['', 'response_policy:agg_max'], []] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: undefined, response: 'agg_max' }, + isKeyless: true + } + } + ]; + + testCases.forEach(testCase => { + it(testCase.name, () => { + assert.deepEqual( + transformCommandReply(testCase.input), + testCase.expected + ); + }); + }); + }); + + testUtils.testWithClient('client.command', async client => { + const result = ((await client.command()).find(command => command.name === 'dbsize')); + assert.equal(result?.name, 'dbsize'); + assert.equal(result?.arity, 1); + assert.equal(result?.policies?.request, 'all_shards'); + assert.equal(result?.policies?.response, 'agg_sum'); + }, GLOBAL.SERVERS.OPEN); +}); \ No newline at end of file diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index d0b713b7b9..d9eaa16cc8 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -1,4 +1,5 @@ import { BasicCommandParser, CommandParser } from '../client/parser'; +import { REQUEST_POLICIES_WITH_DEFAULTS, RequestPolicyWithDefaults, RESPONSE_POLICIES_WITH_DEFAULTS, ResponsePolicyWithDefaults } from '../cluster/request-response-policies'; import { RESP_TYPES } from '../RESP/decoder'; import { UnwrapReply, ArrayReply, BlobStringReply, BooleanReply, CommandArguments, DoubleReply, NullReply, NumberReply, RedisArgument, TuplesReply, MapReply, TypeMapping, Command } from '../RESP/types'; @@ -331,9 +332,10 @@ export type CommandRawReply = [ step: number, categories: Array, tips: Array, - keySpecifications: string + keySpecifications: Array ]; + export type CommandReply = { name: string, arity: number, @@ -342,13 +344,25 @@ export type CommandReply = { lastKeyIndex: number, step: number, categories: Set, - keySpecifications: string + policies: { request: RequestPolicyWithDefaults | undefined, response: ResponsePolicyWithDefaults | undefined } + isKeyless: boolean }; export function transformCommandReply( this: void, - [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories, _tips, keySpecifications]: CommandRawReply + [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories, tips, keySpecifications]: CommandRawReply ): CommandReply { + + const requestPolicyRaw = tips[0]?.replace('request_policy:', ''); + const requestPolicy = requestPolicyRaw && Object.values(REQUEST_POLICIES_WITH_DEFAULTS).includes(requestPolicyRaw as RequestPolicyWithDefaults) + ? requestPolicyRaw as RequestPolicyWithDefaults + : undefined; + + const responsePolicyRaw = tips[1]?.replace('response_policy:', ''); + const responsePolicy = responsePolicyRaw && Object.values(RESPONSE_POLICIES_WITH_DEFAULTS).includes(responsePolicyRaw as ResponsePolicyWithDefaults) + ? responsePolicyRaw as ResponsePolicyWithDefaults + : undefined; + return { name, arity, @@ -357,7 +371,11 @@ export function transformCommandReply( lastKeyIndex, step, categories: new Set(categories), - keySpecifications + policies: { + request: requestPolicy, + response: responsePolicy + }, + isKeyless: keySpecifications.length === 0 }; } From 2dbfd667f3e45b6b69e31a95ccd306e2e37ae284 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 11 Jun 2025 12:15:31 +0300 Subject: [PATCH 6/8] use response policies [skip ci] --- .../dynamic-policy-resolver-factory.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts index 03d26c9859..993d97d9a7 100644 --- a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts +++ b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts @@ -100,10 +100,8 @@ export class DynamicPolicyResolverFactory { : RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED; return { - // request: command.policies.request ?? defaultRequest, - // response: command.policies.response ?? defaultResponse - request: defaultRequest, - response: defaultResponse + request: command.policies.request ?? defaultRequest, + response: command.policies.response ?? defaultResponse }; } } \ No newline at end of file From 9ed1783db10fcdacee7cdfcae5b0fad23d9fb677 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 11 Jun 2025 14:23:44 +0300 Subject: [PATCH 7/8] parse subcommands, update static policies actually fetched from server using the dynamic resolver --- packages/client/lib/cluster/index.ts | 2 + .../dynamic-policy-resolver-factory.ts | 25 +- .../request-response-policies/index.ts | 3 +- .../policies-constants.ts | 2 + .../static-policies-data.ts | 2914 ++++++++++++++++- .../request-response-policies/test.spec.ts | 13 - .../request-response-policies/types.ts | 5 + .../lib/commands/generic-transformers.ts | 14 +- 8 files changed, 2901 insertions(+), 77 deletions(-) diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 3ce50c027d..2c16ba7d9d 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -465,8 +465,10 @@ export default class RedisCluster< commandName: string, fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { + const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; const policyResult = this._policyResolver.resolvePolicy(commandName) + if(policyResult.ok) { //TODO } else { diff --git a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts index 993d97d9a7..750ee491f8 100644 --- a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts +++ b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver-factory.ts @@ -1,9 +1,8 @@ import type { CommandReply } from '../../commands/generic-transformers'; import type { CommandPolicies } from './policies-constants'; import { REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS } from './policies-constants'; -import type { PolicyResolver } from './types'; +import type { PolicyResolver, ModulePolicyRecords } from './types'; import { StaticPolicyResolver } from './static-policy-resolver'; -import type { ModulePolicyRecords } from './static-policies-data'; /** * Function type that returns command information from Redis @@ -28,7 +27,7 @@ export class DynamicPolicyResolverFactory { static async create( commandFetcher: CommandFetcher, fallbackResolver?: PolicyResolver - ): Promise { + ): Promise { const commands = await commandFetcher(); const policies: ModulePolicyRecords = {}; @@ -98,10 +97,28 @@ export class DynamicPolicyResolverFactory { const defaultResponse = isKeyless ? RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS : RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED; + + let subcommands: Record | undefined; + if(command.subcommands.length > 0) { + subcommands = {}; + for (const subcommand of command.subcommands) { + + // Subcommands are in format "parentCommand|subcommand" + const parts = subcommand.name.split("\|") + if(parts.length !== 2) { + throw new Error(`Invalid subcommand name: ${subcommand.name}`); + } + const subcommandName = parts[1]; + + subcommands[subcommandName] = DynamicPolicyResolverFactory.#buildCommandPolicies(subcommand); + } + } return { request: command.policies.request ?? defaultRequest, - response: command.policies.response ?? defaultResponse + response: command.policies.response ?? defaultResponse, + isKeyless, + subcommands }; } } \ No newline at end of file diff --git a/packages/client/lib/cluster/request-response-policies/index.ts b/packages/client/lib/cluster/request-response-policies/index.ts index 24a1761451..e4b2410ba8 100644 --- a/packages/client/lib/cluster/request-response-policies/index.ts +++ b/packages/client/lib/cluster/request-response-policies/index.ts @@ -1,10 +1,9 @@ -export type { Either, PolicyResult, PolicyResolver } from './types'; +export type { Either, PolicyResult, PolicyResolver, ModulePolicyRecords, CommandPolicyRecords } from './types'; export { StaticPolicyResolver } from './static-policy-resolver'; export { DynamicPolicyResolverFactory, type CommandFetcher } from './dynamic-policy-resolver-factory'; export * from './policies-constants'; -export type { ModulePolicyRecords, CommandPolicyRecords } from './static-policies-data'; export { POLICIES } from './static-policies-data'; // export { type CommandRouter } from './command-router'; \ No newline at end of file diff --git a/packages/client/lib/cluster/request-response-policies/policies-constants.ts b/packages/client/lib/cluster/request-response-policies/policies-constants.ts index d27c550d75..4045d9955f 100644 --- a/packages/client/lib/cluster/request-response-policies/policies-constants.ts +++ b/packages/client/lib/cluster/request-response-policies/policies-constants.ts @@ -27,4 +27,6 @@ export type ResponsePolicyWithDefaults = typeof RESPONSE_POLICIES_WITH_DEFAULTS[ export interface CommandPolicies { readonly request: RequestPolicyWithDefaults; readonly response: ResponsePolicyWithDefaults; + readonly subcommands?: Record; + readonly isKeyless: boolean; } \ No newline at end of file diff --git a/packages/client/lib/cluster/request-response-policies/static-policies-data.ts b/packages/client/lib/cluster/request-response-policies/static-policies-data.ts index 68c44c3129..327541fcd6 100644 --- a/packages/client/lib/cluster/request-response-policies/static-policies-data.ts +++ b/packages/client/lib/cluster/request-response-policies/static-policies-data.ts @@ -1,59 +1,2865 @@ -import { CommandPolicies, REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS } from './policies-constants'; - -export type CommandPolicyRecords = Record; -// The response of the COMMAND command uses "." to separate the module name from the command name. -// For example, "ft.search" refers to the "search" command in the "ft" module. It is important to use the same naming convention here. -export type ModulePolicyRecords = Record; +import { ModulePolicyRecords } from "./types"; export const POLICIES: ModulePolicyRecords = { - ft: { - create: { - request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_NODES, - response: RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED - }, - search: { - request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, - response: RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL - }, - aggregate: { - request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, - response: RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL - }, - sugadd: { - request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, - response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED - }, - sugget: { - request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, - response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED - }, - sugdel: { - request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, - response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED - }, - suglen: { - request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, - response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED - }, - spellcheck: { - request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, - response: RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL - }, - cursor: { - request: REQUEST_POLICIES_WITH_DEFAULTS.SPECIAL, - response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS - }, - dictadd: { - request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, - response: RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED - }, - dictdel: { - request: REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS, - response: RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED - }, - dictdump: { - request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, - response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS + "std": { + "getrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "incr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zlexcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hincrbyfloat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zinterstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zpopmax": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zdiff": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "waitaof": { + "request": "all_shards", + "response": "agg_min", + "isKeyless": true + }, + "psubscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "geodist": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "type": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "flushdb": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + }, + "lpos": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xreadgroup": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "pttl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sdiff": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hkeys": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "eval": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "substr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zremrangebyrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "memory": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "purge": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + }, + "doctor": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "stats": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "malloc-stats": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "usage": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + } + }, + "hgetdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hpersist": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "persist": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "llen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "failover": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "hello": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "exec": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "hpexpiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "acl": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "deluser": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "genpass": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "dryrun": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "save": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "cat": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "users": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "whoami": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "load": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "log": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "setuser": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "getuser": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "sort": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "latency": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "history": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "reset": { + "request": "all_nodes", + "response": "agg_sum", + "isKeyless": true + }, + "doctor": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "histogram": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "latest": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "graph": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "zincrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sync": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "rpushx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xtrim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "auth": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "echo": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "georadiusbymember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zcard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "setnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hsetex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "restore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "geoadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "subscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "getex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zremrangebyscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hmset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zremrangebylex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "watch": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "fcall": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hpttl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zintercard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sort_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zrandmember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "discard": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zpopmin": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "scard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hrandfield": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hstrlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xinfo": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "groups": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "consumers": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "stream": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "flushall": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + }, + "linsert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "geopos": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "pexpiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sdiffstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "ping": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + }, + "zscan": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zunionstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "ssubscribe": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zrevrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "slaveof": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "bitcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "evalsha_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lpushx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sinterstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "touch": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false + }, + "bgsave": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "pfcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zdiffstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "pubsub": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "shardnumsub": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "numpat": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "numsub": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "channels": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "shardchannels": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "lindex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "georadiusbymember_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "geohash": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xgroup": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "setid": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "create": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "destroy": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "delconsumer": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "createconsumer": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + } + }, + "xadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sscan": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "randomkey": { + "request": "all_shards", + "response": "special", + "isKeyless": true + }, + "bzpopmax": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "bitfield_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "ttl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hsetnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "rename": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "shutdown": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "strlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hpexpireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "slowlog": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "get": { + "request": "all_nodes", + "response": "default-keyless", + "isKeyless": true + }, + "reset": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "len": { + "request": "all_nodes", + "response": "agg_sum", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "setex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xack": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "client": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "caching": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "setinfo": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "setname": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "kill": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "no-evict": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "reply": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "tracking": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "unblock": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "trackinginfo": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "unpause": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "id": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "getredir": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "pause": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "getname": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "no-touch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "unsubscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "pexpireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hgetall": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "multi": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zrevrangebyscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "psetex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xsetid": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "decr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "rpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xautoclaim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zrangestore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "get": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "blpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "replconf": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "keys": { + "request": "all_shards", + "response": "default-keyless", + "isKeyless": true + }, + "command": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "getkeysandflags": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "count": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "getkeys": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "docs": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "exists": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false + }, + "sismember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "function": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "dump": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "delete": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + }, + "stats": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "restore": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "kill": { + "request": "all_shards", + "response": "one_succeeded", + "isKeyless": true + }, + "load": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + }, + "flush": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true + } + } + }, + "xread": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "rpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "append": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "set": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "move": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "expireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "pexpire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "brpoplpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "del": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false + }, + "lmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "setrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sunsubscribe": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "migrate": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "scan": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "lcs": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "quit": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "cluster": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "addslotsrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "delslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "setslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "slots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "links": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "delslotsrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "addslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "keyslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "meet": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "countkeysinslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "count-failure-reports": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "shards": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "myshardid": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "myid": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "reset": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "flushslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "slaves": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "replicate": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "nodes": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "failover": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "saveconfig": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "getkeysinslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "set-config-epoch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "bumpepoch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "replicas": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "forget": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "spop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xpending": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sunionstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "select": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "sintercard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "srandmember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "bzmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "pfadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "msetnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "expiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "script": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "load": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "kill": { + "request": "all_shards", + "response": "one_succeeded", + "isKeyless": true + }, + "exists": { + "request": "all_shards", + "response": "agg_logical_and", + "isKeyless": true + }, + "flush": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "debug": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "zrem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "save": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "smove": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "spublish": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "fcall_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lrem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "blmove": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lolwut": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "bzpopmin": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hexpire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "ltrim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "asking": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zrevrangebylex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "restore-asking": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "setbit": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "smembers": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "expire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hexpireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "srem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "httl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lastsave": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "hmget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hexists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "module": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "unload": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "load": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "loadex": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "sadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "monitor": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "geosearch": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "copy": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lmove": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "publish": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "bgrewriteaof": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zunion": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hpexpire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "config": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "set": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "resetstat": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + }, + "get": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "rewrite": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true + } + } + }, + "punsubscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zrangebylex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "reset": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "xclaim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "geosearchstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sinter": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "pfdebug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hscan": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "georadius_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "unwatch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "unlink": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false + }, + "renamenx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "brpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zrevrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "object": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "encoding": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "refcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "idletime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "freq": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + }, + "time": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zrangebyscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "rpoplpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hincrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zinter": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "role": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "zrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "pfselftest": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "hexpiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "incrbyfloat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zmscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "zmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "smismember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "xrevrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "bitpos": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "hgetex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "readonly": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "readwrite": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "pfmerge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "dbsize": { + "request": "all_shards", + "response": "agg_sum", + "isKeyless": true + }, + "dump": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "mget": { + "request": "multi_shard", + "response": "default-keyed", + "isKeyless": false + }, + "mset": { + "request": "multi_shard", + "response": "all_succeeded", + "isKeyless": false + }, + "wait": { + "request": "all_shards", + "response": "agg_min", + "isKeyless": true + }, + "xdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "evalsha": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "bitop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "psync": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "getbit": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "georadius": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "getdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "swapdb": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "debug": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "hvals": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "lpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "replicaof": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "eval_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "getset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "decrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "bitfield": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "blmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "sunion": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "FT": { + "ALIASADD": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "ALIASUPDATE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SPELLCHECK": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DICTADD": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_DROPIFX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DROP": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "EXPLAINCLI": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SUGGET": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "SYNADD": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "TAGVALS": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "EXPLAIN": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "ALTER": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "CURSOR": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_LIST": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_CREATEIFNX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DICTDEL": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "ADD": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "ALIASDEL": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SEARCH": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SYNDUMP": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SUGDEL": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "SUGADD": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "_DROPINDEXIFX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SYNUPDATE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "MGET": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GET": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "AGGREGATE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SUGLEN": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "DEL": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_ALIASDELIFX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_ALIASADDIFNX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DROPINDEX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_ALTERIFNX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "PROFILE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "CREATE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "INFO": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DICTDUMP": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + }, + "json": { + "strlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "mget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "set": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "clear": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "arrpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "arrinsert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "objkeys": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "type": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "debug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "strappend": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "get": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "arrtrim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "del": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "mset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "numincrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "forget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "arrlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "arrindex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "nummultby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "objlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "numpowby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "arrappend": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "merge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "toggle": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "resp": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "cms": { + "initbyprob": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "query": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "merge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "initbydim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "bf": { + "loadchunk": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "debug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "madd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "mexists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "insert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "exists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "card": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "reserve": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "scandump": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "ts": { + "mrevrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "mrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "alter": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "revrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "madd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "createrule": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "del": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "mget": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "get": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "create": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "range": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "queryindex": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "deleterule": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "decrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "tdigest": { + "max": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "byrevrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "byrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "create": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "reset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "merge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "trimmed_mean": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "revrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "min": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "cdf": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "rank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "quantile": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "cf": { + "count": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "debug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "exists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "compact": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "loadchunk": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "insertnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "addnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "insert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "reserve": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "scandump": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "mexists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "del": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "topk": { + "list": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "reserve": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "query": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "count": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + } + }, + "search": { + "CLUSTERSET": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "CLUSTERINFO": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "CLUSTERREFRESH": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + }, + "_FT": { + "CONFIG": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SAFEADD": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false + }, + "DEBUG": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "subcommands": { + "INFO_TAGIDX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "VECSIM_INFO": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SPEC_INVIDXES_INFO": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_HNSW": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "TTL_PAUSE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GC_FORCEBGINVOKE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SHARD_CONNECTION_STATES": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GC_STOP_SCHEDULE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_TAGIDX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "SET_MONITOR_EXPIRATION": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_TERMS": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GC_CONTINUE_SCHEDULE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_NUMIDX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GC_CLEAN_NUMERIC": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "HELP": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "FT.AGGREGATE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "TTL": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_SUFFIX_TRIE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DOCINFO": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_FT.AGGREGATE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "WORKERS": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "RESUME_TOPOLOGY_UPDATER": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_NUMIDXTREE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_INVIDX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "CLEAR_PENDING_TOPOLOGY": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "_FT.SEARCH": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "BG_SCAN_CONTROLLER": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "PAUSE_TOPOLOGY_UPDATER": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GIT_SHA": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "IDTODOCID": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "INVIDX_SUMMARY": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_GEOMIDX": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GC_FORCEINVOKE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "TTL_EXPIRE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "FT.SEARCH": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_PHONETIC_HASH": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DOCIDTOID": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DUMP_PREFIX_TRIE": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "DELETE_LOCAL_CURSORS": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "GC_WAIT_FOR_JOBS": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + }, + "NUMIDX_SUMMARY": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true + } + } + } + }, + "timeseries": { + "REFRESHCLUSTER": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true } } } as const; diff --git a/packages/client/lib/cluster/request-response-policies/test.spec.ts b/packages/client/lib/cluster/request-response-policies/test.spec.ts index 2e3f15c389..739d892ecb 100644 --- a/packages/client/lib/cluster/request-response-policies/test.spec.ts +++ b/packages/client/lib/cluster/request-response-policies/test.spec.ts @@ -6,23 +6,10 @@ import RedisJSON from '@redis/json'; import RedisTimeSeries from '@redis/time-series'; describe('Cluster Request-Response Policies', () => { - testUtils.testWithClient('should resolve policies correctly', async client => { - await client.ft.SUGADD('index', 'string', 1); - await client.ft.dictAdd('index', 'foo'); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - modules: { - ft: RediSearch - } - } - }); testUtils.testWithCluster('should resolve policies correctly', async cluster => { await cluster.ft.SUGADD('index', 'string', 1); - await cluster.ft.DICTADD('index', 'foo'); - await cluster.sendCommand(undefined, true, ['ft.dictadd', 'index', 'string']); }, { ...GLOBAL.CLUSTERS.OPEN, diff --git a/packages/client/lib/cluster/request-response-policies/types.ts b/packages/client/lib/cluster/request-response-policies/types.ts index 27d2cae8a7..187c0348c2 100644 --- a/packages/client/lib/cluster/request-response-policies/types.ts +++ b/packages/client/lib/cluster/request-response-policies/types.ts @@ -20,3 +20,8 @@ export interface PolicyResolver { */ withFallback(fallbackResolver: PolicyResolver): PolicyResolver; } + +export type CommandPolicyRecords = Record; +// The response of the COMMAND command uses "." to separate the module name from the command name. +// For example, "ft.search" refers to the "search" command in the "ft" module. It is important to use the same naming convention here. +export type ModulePolicyRecords = Record; diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index d9eaa16cc8..10f7957357 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -332,7 +332,8 @@ export type CommandRawReply = [ step: number, categories: Array, tips: Array, - keySpecifications: Array + keySpecifications: Array, + subcommands: Array ]; @@ -345,14 +346,16 @@ export type CommandReply = { step: number, categories: Set, policies: { request: RequestPolicyWithDefaults | undefined, response: ResponsePolicyWithDefaults | undefined } - isKeyless: boolean + isKeyless: boolean, + subcommands: Array }; export function transformCommandReply( this: void, - [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories, tips, keySpecifications]: CommandRawReply + [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories, tips, keySpecifications, subcommandsReply]: CommandRawReply ): CommandReply { + const requestPolicyRaw = tips[0]?.replace('request_policy:', ''); const requestPolicy = requestPolicyRaw && Object.values(REQUEST_POLICIES_WITH_DEFAULTS).includes(requestPolicyRaw as RequestPolicyWithDefaults) ? requestPolicyRaw as RequestPolicyWithDefaults @@ -363,6 +366,8 @@ export function transformCommandReply( ? responsePolicyRaw as ResponsePolicyWithDefaults : undefined; + const subcommands = subcommandsReply.map(transformCommandReply); + return { name, arity, @@ -375,7 +380,8 @@ export function transformCommandReply( request: requestPolicy, response: responsePolicy }, - isKeyless: keySpecifications.length === 0 + isKeyless: keySpecifications.length === 0, + subcommands }; } From e74bb9a0f1c82d6083335eb6524bc688bd6c55b8 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 11 Jun 2025 16:25:37 +0300 Subject: [PATCH 8/8] fix tests --- .../dynamic-policy-resolver.spec.ts | 27 +++++++++++------ packages/client/lib/commands/COMMAND.spec.ts | 30 +++++++++++-------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts index f1a4feb16b..6f05b62111 100644 --- a/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts +++ b/packages/client/lib/cluster/request-response-policies/dynamic-policy-resolver.spec.ts @@ -21,7 +21,8 @@ describe('DynamicPolicyResolverFactory', () => { std: { ping: { request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, - response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + isKeyless: true } } }); @@ -50,7 +51,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 0, categories: new Set(), policies: { request: undefined, response: undefined }, - isKeyless: true + isKeyless: true, + subcommands: [] } ]; @@ -76,7 +78,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 1, categories: new Set(), policies: { request: undefined, response: undefined }, - isKeyless: false + isKeyless: false, + subcommands: [] } ]; @@ -102,7 +105,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 0, categories: new Set(), policies: { request: 'all_shards', response: 'agg_sum' }, - isKeyless: true + isKeyless: true, + subcommands: [] } ]; @@ -128,7 +132,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 1, categories: new Set(), policies: { request: 'all_shards', response: 'special' }, - isKeyless: false + isKeyless: false, + subcommands: [] } ]; @@ -154,7 +159,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 0, categories: new Set(), policies: { request: undefined, response: undefined }, - isKeyless: true + isKeyless: true, + subcommands: [] } ]; @@ -182,7 +188,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 0, categories: new Set(), policies: { request: undefined, response: undefined }, - isKeyless: true + isKeyless: true, + subcommands: [] } ]; @@ -233,7 +240,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 0, categories: new Set(), policies: { request: 'all_nodes', response: undefined }, - isKeyless: false + isKeyless: false, + subcommands: [] }, { name: 'partial-response', @@ -244,7 +252,8 @@ describe('DynamicPolicyResolverFactory', () => { step: 0, categories: new Set(), policies: { request: undefined, response: 'agg_sum' }, - isKeyless: true + isKeyless: true, + subcommands: [] } ]; diff --git a/packages/client/lib/commands/COMMAND.spec.ts b/packages/client/lib/commands/COMMAND.spec.ts index 8a3900d9d6..7ace4de3c7 100644 --- a/packages/client/lib/commands/COMMAND.spec.ts +++ b/packages/client/lib/commands/COMMAND.spec.ts @@ -15,7 +15,7 @@ describe('COMMAND', () => { const testCases = [ { name: 'without policies', - input: ['ping', -1, [CommandFlags.STALE], 0, 0, 0, [CommandCategories.FAST], [], []] satisfies CommandRawReply, + input: ['ping', -1, [CommandFlags.STALE], 0, 0, 0, [CommandCategories.FAST], [], [], []] satisfies CommandRawReply, expected: { name: 'ping', arity: -1, @@ -25,12 +25,13 @@ describe('COMMAND', () => { step: 0, categories: new Set([CommandCategories.FAST]), policies: { request: undefined, response: undefined }, - isKeyless: true + isKeyless: true, + subcommands: [] } }, { name: 'with valid policies', - input: ['dbsize', 1, [], 0, 0, 0, [], ['request_policy:all_shards', 'response_policy:agg_sum'], []] satisfies CommandRawReply, + input: ['dbsize', 1, [], 0, 0, 0, [], ['request_policy:all_shards', 'response_policy:agg_sum'], [], []] satisfies CommandRawReply, expected: { name: 'dbsize', arity: 1, @@ -40,12 +41,13 @@ describe('COMMAND', () => { step: 0, categories: new Set([]), policies: { request: 'all_shards', response: 'agg_sum' }, - isKeyless: true + isKeyless: true, + subcommands: [] } }, { name: 'with invalid policies', - input: ['test', 0, [], 0, 0, 0, [], ['request_policy:invalid', 'response_policy:invalid'], ['some key specification']] satisfies CommandRawReply, + input: ['test', 0, [], 0, 0, 0, [], ['request_policy:invalid', 'response_policy:invalid'], ['some key specification'], []] satisfies CommandRawReply, expected: { name: 'test', arity: 0, @@ -55,12 +57,13 @@ describe('COMMAND', () => { step: 0, categories: new Set([]), policies: { request: undefined, response: undefined }, - isKeyless: false + isKeyless: false, + subcommands: [] } }, { name: 'with request policy only', - input: ['test', 0, [], 0, 0, 0, [], ['request_policy:all_nodes'], ['some key specification']] satisfies CommandRawReply, + input: ['test', 0, [], 0, 0, 0, [], ['request_policy:all_nodes'], ['some key specification'], []] satisfies CommandRawReply, expected: { name: 'test', arity: 0, @@ -70,12 +73,13 @@ describe('COMMAND', () => { step: 0, categories: new Set([]), policies: { request: 'all_nodes', response: undefined }, - isKeyless: false + isKeyless: false, + subcommands: [] } }, { name: 'with response policy only', - input: ['test', 0, [], 0, 0, 0, [], ['', 'response_policy:agg_max'], []] satisfies CommandRawReply, + input: ['test', 0, [], 0, 0, 0, [], ['', 'response_policy:agg_max'], [], []] satisfies CommandRawReply, expected: { name: 'test', arity: 0, @@ -85,12 +89,13 @@ describe('COMMAND', () => { step: 0, categories: new Set([]), policies: { request: undefined, response: 'agg_max' }, - isKeyless: true + isKeyless: true, + subcommands: [] } }, { name: 'with response policy only', - input: ['test', 0, [], 0, 0, 0, [], ['', 'response_policy:agg_max'], []] satisfies CommandRawReply, + input: ['test', 0, [], 0, 0, 0, [], ['', 'response_policy:agg_max'], [], []] satisfies CommandRawReply, expected: { name: 'test', arity: 0, @@ -100,7 +105,8 @@ describe('COMMAND', () => { step: 0, categories: new Set([]), policies: { request: undefined, response: 'agg_max' }, - isKeyless: true + isKeyless: true, + subcommands: [] } } ];