diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 8b6ea03ee8f..508e9ab38e6 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -1239,6 +1239,61 @@ describe('Client', () => { } }); + describe('read-only scripts', () => { + testUtils.isVersionGreaterThanHook([7]); + + const READ_ONLY_SQUARE_SCRIPT = defineScript({ + ...SQUARE_SCRIPT, + IS_READ_ONLY: true + }); + + const options = { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + scripts: { + square: READ_ONLY_SQUARE_SCRIPT + } + } + }; + + // runs fn as a user that may run EVAL_RO and EVALSHA_RO but not EVAL or EVALSHA, + // after SCRIPT FLUSH, so the script has to be loaded again + async function asReadOnlyScriptsUser( + client: RedisClientType<{}, {}, { square: typeof READ_ONLY_SQUARE_SCRIPT }, 2>, + fn: (user: typeof client) => Promise + ) { + await client.sendCommand([ + 'ACL', 'SETUSER', 'read-only-scripts', 'on', '>password', '~*', '+@all', '-eval', '-evalsha' + ]); + const user = client.duplicate({ + username: 'read-only-scripts', + password: 'password' + }); + await user.connect(); + + try { + await client.set('key', '2'); + await client.scriptFlush(); + await fn(user); + } finally { + user.destroy(); + await client.sendCommand(['ACL', 'DELUSER', 'read-only-scripts']); + } + } + + testUtils.testWithClient('loads a missing script with EVAL_RO', async client => { + await asReadOnlyScriptsUser(client, async user => { + assert.equal(await user.square('key'), 4); + }); + }, options); + + testUtils.testWithClient('uses EVAL_RO in MULTI', async client => { + await asReadOnlyScriptsUser(client, async user => { + assert.deepEqual(await user.multi().square('key').exec(), [4]); + }); + }, options); + }); + const module = { echo: { parseCommand(parser: CommandParser, message: string) { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index e6043379351..ce39d7b52e5 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -1725,7 +1725,7 @@ export default class RedisClient< } catch (err) { if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err; - args[0] = 'EVAL'; + args[0] = script.IS_READ_ONLY ? 'EVAL_RO' : 'EVAL'; args[1] = script.SCRIPT; reply = await this.sendCommand(args, options); } diff --git a/packages/client/lib/multi-command.spec.ts b/packages/client/lib/multi-command.spec.ts index 7e77f88d10b..4afa458aec2 100644 --- a/packages/client/lib/multi-command.spec.ts +++ b/packages/client/lib/multi-command.spec.ts @@ -42,6 +42,24 @@ describe('Multi Command', () => { ['EVALSHA', SQUARE_SCRIPT.SHA1, '2'] ); }); + + it('should use EVAL_RO and EVALSHA_RO for a read-only script', () => { + const multi = new RedisMultiCommand(), + script = { + ...SQUARE_SCRIPT, + IS_READ_ONLY: true + }; + + multi.addScript(script, ['1']); + multi.addScript(script, ['2']); + assert.deepEqual( + multi.queue.map(({ args }) => Array.from(args)), + [ + ['EVAL_RO', SQUARE_SCRIPT.SCRIPT, '1', '1'], + ['EVALSHA_RO', SQUARE_SCRIPT.SHA1, '1', '2'] + ] + ); + }); }); describe('exec', () => { diff --git a/packages/client/lib/multi-command.ts b/packages/client/lib/multi-command.ts index bb30fddc29e..5986e0c5a2a 100644 --- a/packages/client/lib/multi-command.ts +++ b/packages/client/lib/multi-command.ts @@ -44,10 +44,10 @@ export default class RedisMultiCommand { const redisArgs: CommandArguments = []; redisArgs.preserve = args.preserve; if (this.scriptsInUse.has(script.SHA1)) { - redisArgs.push('EVALSHA', script.SHA1); + redisArgs.push(script.IS_READ_ONLY ? 'EVALSHA_RO' : 'EVALSHA', script.SHA1); } else { this.scriptsInUse.add(script.SHA1); - redisArgs.push('EVAL', script.SCRIPT); + redisArgs.push(script.IS_READ_ONLY ? 'EVAL_RO' : 'EVAL', script.SCRIPT); } if (script.NUMBER_OF_KEYS !== undefined) {