diff --git a/packages/client/lib/commands/ACL_GETUSER.spec.ts b/packages/client/lib/commands/ACL_GETUSER.spec.ts index d80cb892bb4..b1d1d334c5b 100644 --- a/packages/client/lib/commands/ACL_GETUSER.spec.ts +++ b/packages/client/lib/commands/ACL_GETUSER.spec.ts @@ -13,6 +13,13 @@ describe('ACL GETUSER', () => { ); }); + it('transformReply passes a null reply through for nonexistent users', () => { + assert.equal( + (ACL_GETUSER.transformReply[2] as unknown as (reply: unknown) => unknown)(null), + null + ); + }); + testUtils.testWithClient('client.aclGetUser', async client => { const reply = await client.aclGetUser('default'); diff --git a/packages/client/lib/commands/ACL_GETUSER.ts b/packages/client/lib/commands/ACL_GETUSER.ts index d0c2572ee98..8257f9c7412 100644 --- a/packages/client/lib/commands/ACL_GETUSER.ts +++ b/packages/client/lib/commands/ACL_GETUSER.ts @@ -1,5 +1,6 @@ import { CommandParser } from '../client/parser'; -import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command, NullReply } from '../RESP/types'; +import { isNullReply } from './generic-transformers'; type AclUser = TuplesToMapReply<[ [BlobStringReply<'flags'>, ArrayReply], @@ -23,21 +24,26 @@ export default { parser.push('ACL', 'GETUSER', username); }, transformReply: { - 2: (reply: UnwrapReply>) => ({ - flags: reply[1], - passwords: reply[3], - commands: reply[5], - keys: reply[7], - channels: reply[9], - selectors: (reply[11] as unknown as UnwrapReply)?.map(selector => { - const inferred = selector as unknown as UnwrapReply; - return { - commands: inferred[1], - keys: inferred[3], - channels: inferred[5] - }; - }) - }), - 3: undefined as unknown as () => AclUser + // NullReply when the user does not exist + 2: (reply: UnwrapReply> | NullReply) => { + if (isNullReply(reply)) return reply; + + return { + flags: reply[1], + passwords: reply[3], + commands: reply[5], + keys: reply[7], + channels: reply[9], + selectors: (reply[11] as unknown as UnwrapReply)?.map(selector => { + const inferred = selector as unknown as UnwrapReply; + return { + commands: inferred[1], + keys: inferred[3], + channels: inferred[5] + }; + }) + }; + }, + 3: undefined as unknown as () => AclUser | NullReply } } as const satisfies Command; diff --git a/packages/client/types-tests/acl-getuser.types-test.ts b/packages/client/types-tests/acl-getuser.types-test.ts new file mode 100644 index 00000000000..a1f8f5d426a --- /dev/null +++ b/packages/client/types-tests/acl-getuser.types-test.ts @@ -0,0 +1,22 @@ +/** + * Compile-time regression for https://github.com/redis/node-redis/issues/2745. + * + * ACL GETUSER replies null for a nonexistent user, but the declared reply type + * claimed a non-null object shape and the RESP2 transformer indexed into the + * null reply. These assertions fail to compile against the unfixed code. + * + * Lives outside lib/ so it is not picked up by the production build / typedoc. + * Checked with `npm run test:types -w @redis/client`. + */ +import type { CommandReply, ReplyWithTypeMapping } from '../lib/RESP/types'; +import type ACL_GETUSER from '../lib/commands/ACL_GETUSER'; + +type Assert = T; + +// The default (RESP3) client-facing reply type must include null. +type Resp3ClientReply = ReplyWithTypeMapping, {}>; +export type Resp3ClientReplyIncludesNull = Assert; + +// The RESP2 client-facing reply type must include null too. +type Resp2ClientReply = ReplyWithTypeMapping, {}>; +export type Resp2ClientReplyIncludesNull = Assert;