diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 99181e08e5e..84d70d6b4c0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: node-version: ['18', '20'] - redis-version: ['5', '6.0', '6.2', '7.0', '7.2'] + redis-version: ['5', '6.0', '6.2', '7.0', '7.2', '7.4-rc2'] steps: - uses: actions/checkout@v4 with: diff --git a/package-lock.json b/package-lock.json index 5cae28a971f..18a7003947e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,23 @@ { "name": "redis", - "version": "4.6.13", + "version": "4.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "redis", - "version": "4.6.13", + "version": "4.7.0", "license": "MIT", "workspaces": [ "./packages/*" ], "dependencies": { "@redis/bloom": "1.2.0", - "@redis/client": "1.5.14", + "@redis/client": "1.6.0", "@redis/graph": "1.1.1", - "@redis/json": "1.0.6", - "@redis/search": "1.1.6", - "@redis/time-series": "1.0.5" + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" }, "devDependencies": { "@tsconfig/node14": "^14.1.0", @@ -8838,7 +8838,7 @@ }, "packages/client": { "name": "@redis/client", - "version": "1.5.14", + "version": "1.6.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2", @@ -8887,7 +8887,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "1.0.6", + "version": "1.0.7", "license": "MIT", "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -8906,7 +8906,7 @@ }, "packages/search": { "name": "@redis/search", - "version": "1.1.6", + "version": "1.2.0", "license": "MIT", "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -8943,7 +8943,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "1.0.5", + "version": "1.1.0", "license": "MIT", "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", diff --git a/package.json b/package.json index 07e1f13484d..e8ceef7173d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "4.6.13", + "version": "4.7.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -24,11 +24,11 @@ }, "dependencies": { "@redis/bloom": "1.2.0", - "@redis/client": "1.5.14", + "@redis/client": "1.6.0", "@redis/graph": "1.1.1", - "@redis/json": "1.0.6", - "@redis/search": "1.1.6", - "@redis/time-series": "1.0.5" + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" }, "devDependencies": { "@tsconfig/node14": "^14.1.0", diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 4442d3adb83..7f93efaa1c3 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -788,6 +788,31 @@ describe('Client', () => { assert.deepEqual(hash, results); }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('hScanNoValuesIterator', async client => { + const hash: Record = {}; + const expectedKeys: Array = []; + for (let i = 0; i < 100; i++) { + hash[i.toString()] = i.toString(); + expectedKeys.push(i.toString()); + } + + await client.hSet('key', hash); + + const keys: Array = []; + for await (const key of client.hScanNoValuesIterator('key')) { + keys.push(key); + } + + function sort(a: string, b: string) { + return Number(a) - Number(b); + } + + assert.deepEqual(keys.sort(sort), expectedKeys); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [7, 4] + }); + testUtils.testWithClient('sScanIterator', async client => { const members = new Set(); for (let i = 0; i < 100; i++) { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 4c3964c7aa0..d7f33e97b16 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -1,5 +1,5 @@ import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands'; +import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands'; import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket'; import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; @@ -820,6 +820,17 @@ export default class RedisClient< } while (cursor !== 0); } + async* hScanNoValuesIterator(key: string, options?: ScanOptions): AsyncIterable> { + let cursor = 0; + do { + const reply = await (this as any).hScanNoValues(key, cursor, options); + cursor = reply.cursor; + for (const k of reply.keys) { + yield k; + } + } while (cursor !== 0); + } + async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable { let cursor = 0; do { diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index b1cc49b4c82..45c96a80b50 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -158,13 +158,13 @@ export default class RedisClusterSlots< } async #discover(rootNode?: RedisClusterClientOptions) { - this.#resetSlots(); const addressesInUse = new Set(); try { const shards = await this.#getShards(rootNode), promises: Array> = [], eagerConnect = this.#options.minimizeConnections !== true; + this.#resetSlots(); for (const { from, to, master, replicas } of shards) { const shard: Shard = { master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises) @@ -269,10 +269,10 @@ export default class RedisClusterSlots< if (this.#options.defaults) { let socket; if (this.#options.defaults.socket) { - socket = options?.socket ? { + socket = { ...this.#options.defaults.socket, - ...options.socket - } : this.#options.defaults.socket; + ...options?.socket + }; } else { socket = options?.socket; } diff --git a/packages/client/lib/cluster/commands.ts b/packages/client/lib/cluster/commands.ts index 84a37862772..9027c5c0b5e 100644 --- a/packages/client/lib/cluster/commands.ts +++ b/packages/client/lib/cluster/commands.ts @@ -53,6 +53,9 @@ import * as GETRANGE from '../commands/GETRANGE'; import * as GETSET from '../commands/GETSET'; import * as HDEL from '../commands/HDEL'; import * as HEXISTS from '../commands/HEXISTS'; +import * as HEXPIRE from '../commands/HEXPIRE'; +import * as HEXPIREAT from '../commands/HEXPIREAT'; +import * as HEXPIRETIME from '../commands/HEXPIRETIME'; import * as HGET from '../commands/HGET'; import * as HGETALL from '../commands/HGETALL'; import * as HINCRBY from '../commands/HINCRBY'; @@ -60,13 +63,20 @@ import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT'; import * as HKEYS from '../commands/HKEYS'; import * as HLEN from '../commands/HLEN'; import * as HMGET from '../commands/HMGET'; +import * as HPERSIST from '../commands/HPERSIST'; +import * as HPEXPIRE from '../commands/HPEXPIRE'; +import * as HPEXPIREAT from '../commands/HPEXPIREAT'; +import * as HPEXPIRETIME from '../commands/HPEXPIRETIME'; +import * as HPTTL from '../commands/HPTTL'; import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES'; import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT'; import * as HRANDFIELD from '../commands/HRANDFIELD'; import * as HSCAN from '../commands/HSCAN'; +import * as HSCAN_NOVALUES from '../commands/HSCAN_NOVALUES'; import * as HSET from '../commands/HSET'; import * as HSETNX from '../commands/HSETNX'; import * as HSTRLEN from '../commands/HSTRLEN'; +import * as HTTL from '../commands/HTTL'; import * as HVALS from '../commands/HVALS'; import * as INCR from '../commands/INCR'; import * as INCRBY from '../commands/INCRBY'; @@ -321,6 +331,12 @@ export default { hDel: HDEL, HEXISTS, hExists: HEXISTS, + HEXPIRE, + hExpire: HEXPIRE, + HEXPIREAT, + hExpireAt: HEXPIREAT, + HEXPIRETIME, + hExpireTime: HEXPIRETIME, HGET, hGet: HGET, HGETALL, @@ -335,6 +351,16 @@ export default { hLen: HLEN, HMGET, hmGet: HMGET, + HPERSIST, + hPersist: HPERSIST, + HPEXPIRE, + hpExpire: HPEXPIRE, + HPEXPIREAT, + hpExpireAt: HPEXPIREAT, + HPEXPIRETIME, + hpExpireTime: HPEXPIRETIME, + HPTTL, + hpTTL: HPTTL, HRANDFIELD_COUNT_WITHVALUES, hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES, HRANDFIELD_COUNT, @@ -343,12 +369,16 @@ export default { hRandField: HRANDFIELD, HSCAN, hScan: HSCAN, + HSCAN_NOVALUES, + hScanNoValues: HSCAN_NOVALUES, HSET, hSet: HSET, HSETNX, hSetNX: HSETNX, HSTRLEN, hStrLen: HSTRLEN, + HTTL, + hTTL: HTTL, HVALS, hVals: HVALS, INCR, diff --git a/packages/client/lib/commands/CLIENT_KILL.spec.ts b/packages/client/lib/commands/CLIENT_KILL.spec.ts index 2fe894f3610..733aaca858b 100644 --- a/packages/client/lib/commands/CLIENT_KILL.spec.ts +++ b/packages/client/lib/commands/CLIENT_KILL.spec.ts @@ -65,6 +65,16 @@ describe('CLIENT KILL', () => { ); }); + it('MAXAGE', () => { + assert.deepEqual( + transformArguments({ + filter: ClientKillFilters.MAXAGE, + maxAge: 10 + }), + ['CLIENT', 'KILL', 'MAXAGE', '10'] + ); + }); + describe('SKIP_ME', () => { it('undefined', () => { assert.deepEqual( diff --git a/packages/client/lib/commands/CLIENT_KILL.ts b/packages/client/lib/commands/CLIENT_KILL.ts index adb2a5a6569..b1a53df64d8 100644 --- a/packages/client/lib/commands/CLIENT_KILL.ts +++ b/packages/client/lib/commands/CLIENT_KILL.ts @@ -6,7 +6,8 @@ export enum ClientKillFilters { ID = 'ID', TYPE = 'TYPE', USER = 'USER', - SKIP_ME = 'SKIPME' + SKIP_ME = 'SKIPME', + MAXAGE = 'MAXAGE' } interface KillFilter { @@ -37,7 +38,11 @@ type KillSkipMe = ClientKillFilters.SKIP_ME | (KillFilter { + maxAge: number; +} + +type KillFilters = KillAddress | KillLocalAddress | KillId | KillType | KillUser | KillSkipMe | KillMaxAge; export function transformArguments(filters: KillFilters | Array): RedisCommandArguments { const args = ['CLIENT', 'KILL']; @@ -89,6 +94,10 @@ function pushFilter(args: RedisCommandArguments, filter: KillFilters): void { case ClientKillFilters.SKIP_ME: args.push(filter.skipMe ? 'yes' : 'no'); break; + + case ClientKillFilters.MAXAGE: + args.push(filter.maxAge.toString()); + break; } } diff --git a/packages/client/lib/commands/HEXPIRE.spec.ts b/packages/client/lib/commands/HEXPIRE.spec.ts new file mode 100644 index 00000000000..3714f617f58 --- /dev/null +++ b/packages/client/lib/commands/HEXPIRE.spec.ts @@ -0,0 +1,40 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HEXPIRE'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HEXPIRE', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field', 1), + ['HEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2'], 1), + ['HEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + + it('with set option', () => { + assert.deepEqual( + transformArguments('key', ['field1'], 1, 'NX'), + ['HEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1'] + ); + }); + }); + + testUtils.testWithClient('hexpire', async client => { + assert.deepEqual( + await client.hExpire('key', ['field1'], 0), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts new file mode 100644 index 00000000000..938f9039939 --- /dev/null +++ b/packages/client/lib/commands/HEXPIRE.ts @@ -0,0 +1,44 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument } from './generic-transformers'; + +/** + * @readonly + * @enum {number} + */ +export const HASH_EXPIRATION = { + /** @property {number} */ + /** The field does not exist */ + FIELD_NOT_EXISTS: -2, + /** @property {number} */ + /** Specified NX | XX | GT | LT condition not met */ + CONDITION_NOT_MET: 0, + /** @property {number} */ + /** Expiration time was set or updated */ + UPDATED: 1, + /** @property {number} */ + /** Field deleted because the specified expiration time is in the past */ + DELETED: 2 +} as const; + +export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION]; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments( + key: RedisCommandArgument, + fields: RedisCommandArgument| Array, + seconds: number, + mode?: 'NX' | 'XX' | 'GT' | 'LT', +) { + const args = ['HEXPIRE', key, seconds.toString()]; + + if (mode) { + args.push(mode); + } + + args.push('FIELDS'); + + return pushVerdictArgument(args, fields); +} + +export declare function transformReply(): Array; \ No newline at end of file diff --git a/packages/client/lib/commands/HEXPIREAT.spec.ts b/packages/client/lib/commands/HEXPIREAT.spec.ts new file mode 100644 index 00000000000..1c65fb61773 --- /dev/null +++ b/packages/client/lib/commands/HEXPIREAT.spec.ts @@ -0,0 +1,49 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HEXPIREAT'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HEXPIREAT', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string + number', () => { + assert.deepEqual( + transformArguments('key', 'field', 1), + ['HEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] + ); + }); + + it('array + number', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2'], 1), + ['HEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + + it('date', () => { + const d = new Date(); + + assert.deepEqual( + transformArguments('key', ['field1'], d), + ['HEXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString(), 'FIELDS', '1', 'field1'] + ); + }); + + it('with set option', () => { + assert.deepEqual( + transformArguments('key', 'field1', 1, 'GT'), + ['HEXPIREAT', 'key', '1', 'GT', 'FIELDS', '1', 'field1'] + ); + }); + }); + + testUtils.testWithClient('expireAt', async client => { + assert.deepEqual( + await client.hExpireAt('key', 'field1', 1), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN, + }); +}); diff --git a/packages/client/lib/commands/HEXPIREAT.ts b/packages/client/lib/commands/HEXPIREAT.ts new file mode 100644 index 00000000000..58c52d3a1f6 --- /dev/null +++ b/packages/client/lib/commands/HEXPIREAT.ts @@ -0,0 +1,28 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument, transformEXAT } from './generic-transformers'; +import { HashExpiration } from './HEXPIRE'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments( + key: RedisCommandArgument, + fields: RedisCommandArgument | Array, + timestamp: number | Date, + mode?: 'NX' | 'XX' | 'GT' | 'LT' +) { + const args = [ + 'HEXPIREAT', + key, + transformEXAT(timestamp) + ]; + + if (mode) { + args.push(mode); + } + + args.push('FIELDS') + + return pushVerdictArgument(args, fields); +} + +export declare function transformReply(): Array; \ No newline at end of file diff --git a/packages/client/lib/commands/HEXPIRETIME.spec.ts b/packages/client/lib/commands/HEXPIRETIME.spec.ts new file mode 100644 index 00000000000..9c3eb024bed --- /dev/null +++ b/packages/client/lib/commands/HEXPIRETIME.spec.ts @@ -0,0 +1,32 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { HASH_EXPIRATION_TIME, transformArguments } from './HEXPIRETIME'; + +describe('HEXPIRETIME', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HEXPIRETIME', 'key', 'FIELDS', '1', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2']), + ['HEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + }) + + testUtils.testWithClient('hExpireTime', async client => { + assert.deepEqual( + await client.hExpireTime('key', 'field1'), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN, + }); +}); diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts new file mode 100644 index 00000000000..01764b1032d --- /dev/null +++ b/packages/client/lib/commands/HEXPIRETIME.ts @@ -0,0 +1,21 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument } from './generic-transformers'; + +export const HASH_EXPIRATION_TIME = { + /** @property {number} */ + /** The field does not exist */ + FIELD_NOT_EXISTS: -2, + /** @property {number} */ + /** The field exists but has no associated expire */ + NO_EXPIRATION: -1, +} as const; + +export const FIRST_KEY_INDEX = 1 + +export const IS_READ_ONLY = true; + +export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { + return pushVerdictArgument(['HEXPIRETIME', key, 'FIELDS'], fields); +} + +export declare function transformReply(): Array; \ No newline at end of file diff --git a/packages/client/lib/commands/HPERSIST.spec.ts b/packages/client/lib/commands/HPERSIST.spec.ts new file mode 100644 index 00000000000..8cf3f1fe221 --- /dev/null +++ b/packages/client/lib/commands/HPERSIST.spec.ts @@ -0,0 +1,33 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HPERSIST'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HPERSIST', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HPERSIST', 'key', 'FIELDS', '1', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2']), + ['HPERSIST', 'key', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + }) + + testUtils.testWithClient('hPersist', async client => { + assert.deepEqual( + await client.hPersist('key', 'field1'), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN, + }); +}); diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts new file mode 100644 index 00000000000..862a7548ac1 --- /dev/null +++ b/packages/client/lib/commands/HPERSIST.ts @@ -0,0 +1,10 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { + return pushVerdictArgument(['HPERSIST', key, 'FIELDS'], fields); +} + +export declare function transformReply(): Array | null; \ No newline at end of file diff --git a/packages/client/lib/commands/HPEXPIRE.spec.ts b/packages/client/lib/commands/HPEXPIRE.spec.ts new file mode 100644 index 00000000000..852d9f5bd21 --- /dev/null +++ b/packages/client/lib/commands/HPEXPIRE.spec.ts @@ -0,0 +1,40 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HPEXPIRE'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HEXPIRE', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field', 1), + ['HPEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2'], 1), + ['HPEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + + it('with set option', () => { + assert.deepEqual( + transformArguments('key', ['field1'], 1, 'NX'), + ['HPEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1'] + ); + }); + }); + + testUtils.testWithClient('hexpire', async client => { + assert.deepEqual( + await client.hpExpire('key', ['field1'], 0), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts new file mode 100644 index 00000000000..afbb056ed4e --- /dev/null +++ b/packages/client/lib/commands/HPEXPIRE.ts @@ -0,0 +1,24 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument } from './generic-transformers'; +import { HashExpiration } from "./HEXPIRE"; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments( + key: RedisCommandArgument, + fields: RedisCommandArgument | Array, + ms: number, + mode?: 'NX' | 'XX' | 'GT' | 'LT', +) { + const args = ['HPEXPIRE', key, ms.toString()]; + + if (mode) { + args.push(mode); + } + + args.push('FIELDS') + + return pushVerdictArgument(args, fields); +} + +export declare function transformReply(): Array | null; \ No newline at end of file diff --git a/packages/client/lib/commands/HPEXPIREAT.spec.ts b/packages/client/lib/commands/HPEXPIREAT.spec.ts new file mode 100644 index 00000000000..9747cca1a2d --- /dev/null +++ b/packages/client/lib/commands/HPEXPIREAT.spec.ts @@ -0,0 +1,48 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HPEXPIREAT'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HPEXPIREAT', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string + number', () => { + assert.deepEqual( + transformArguments('key', 'field', 1), + ['HPEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] + ); + }); + + it('array + number', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2'], 1), + ['HPEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + + it('date', () => { + const d = new Date(); + assert.deepEqual( + transformArguments('key', ['field1'], d), + ['HPEXPIREAT', 'key', d.getTime().toString(), 'FIELDS', '1', 'field1'] + ); + }); + + it('with set option', () => { + assert.deepEqual( + transformArguments('key', ['field1'], 1, 'XX'), + ['HPEXPIREAT', 'key', '1', 'XX', 'FIELDS', '1', 'field1'] + ); + }); + }); + + testUtils.testWithClient('hpExpireAt', async client => { + assert.deepEqual( + await client.hpExpireAt('key', ['field1'], 1), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN, + }); +}); diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts new file mode 100644 index 00000000000..b6e01d8ee5c --- /dev/null +++ b/packages/client/lib/commands/HPEXPIREAT.ts @@ -0,0 +1,25 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument, transformEXAT, transformPXAT } from './generic-transformers'; +import { HashExpiration } from './HEXPIRE'; + +export const FIRST_KEY_INDEX = 1; +export const IS_READ_ONLY = true; + +export function transformArguments( + key: RedisCommandArgument, + fields: RedisCommandArgument | Array, + timestamp: number | Date, + mode?: 'NX' | 'XX' | 'GT' | 'LT' +) { + const args = ['HPEXPIREAT', key, transformPXAT(timestamp)]; + + if (mode) { + args.push(mode); + } + + args.push('FIELDS') + + return pushVerdictArgument(args, fields); +} + +export declare function transformReply(): Array | null; \ No newline at end of file diff --git a/packages/client/lib/commands/HPEXPIRETIME.spec.ts b/packages/client/lib/commands/HPEXPIRETIME.spec.ts new file mode 100644 index 00000000000..ff03b73c71d --- /dev/null +++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts @@ -0,0 +1,33 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HPEXPIRETIME'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HPEXPIRETIME', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HPEXPIRETIME', 'key', 'FIELDS', '1', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2']), + ['HPEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + }); + + testUtils.testWithClient('hpExpireTime', async client => { + assert.deepEqual( + await client.hpExpireTime('key', 'field1'), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts new file mode 100644 index 00000000000..22a794ccefa --- /dev/null +++ b/packages/client/lib/commands/HPEXPIRETIME.ts @@ -0,0 +1,11 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; +export const IS_READ_ONLY = true; + +export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { + return pushVerdictArgument(['HPEXPIRETIME', key, 'FIELDS'], fields); +} + +export declare function transformReply(): Array | null; \ No newline at end of file diff --git a/packages/client/lib/commands/HPTTL.spec.ts b/packages/client/lib/commands/HPTTL.spec.ts new file mode 100644 index 00000000000..ddca26ea85b --- /dev/null +++ b/packages/client/lib/commands/HPTTL.spec.ts @@ -0,0 +1,33 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HPTTL'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HPTTL', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HPTTL', 'key', 'FIELDS', '1', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2']), + ['HPTTL', 'key', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + }); + + testUtils.testWithClient('hpTTL', async client => { + assert.deepEqual( + await client.hpTTL('key', 'field1'), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts new file mode 100644 index 00000000000..988b805c0c9 --- /dev/null +++ b/packages/client/lib/commands/HPTTL.ts @@ -0,0 +1,11 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; +export const IS_READ_ONLY = true; + +export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { + return pushVerdictArgument(['HPTTL', key, 'FIELDS'], fields); +} + +export declare function transformReply(): Array | null; diff --git a/packages/client/lib/commands/HSCAN.spec.ts b/packages/client/lib/commands/HSCAN.spec.ts index b426763b99b..6757888a875 100644 --- a/packages/client/lib/commands/HSCAN.spec.ts +++ b/packages/client/lib/commands/HSCAN.spec.ts @@ -73,5 +73,18 @@ describe('HSCAN', () => { tuples: [] } ); + + await Promise.all([ + client.hSet('key', 'a', '1'), + client.hSet('key', 'b', '2') + ]); + + assert.deepEqual( + await client.hScan('key', 0), + { + cursor: 0, + tuples: [{field: 'a', value: '1'}, {field: 'b', value: '2'}] + } + ); }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/HSCAN.ts b/packages/client/lib/commands/HSCAN.ts index ba18fb986bc..5167693b604 100644 --- a/packages/client/lib/commands/HSCAN.ts +++ b/packages/client/lib/commands/HSCAN.ts @@ -16,7 +16,7 @@ export function transformArguments( ], cursor, options); } -type HScanRawReply = [RedisCommandArgument, Array]; +export type HScanRawReply = [RedisCommandArgument, Array]; export interface HScanTuple { field: RedisCommandArgument; diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts new file mode 100644 index 00000000000..7e05b841e43 --- /dev/null +++ b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts @@ -0,0 +1,79 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments, transformReply } from './HSCAN_NOVALUES'; + +describe('HSCAN_NOVALUES', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments('key', 0), + ['HSCAN', 'key', '0', 'NOVALUES'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern' + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'NOVALUES'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'COUNT', '1', 'NOVALUES'] + ); + }); + }); + + describe('transformReply', () => { + it('without keys', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + keys: [] + } + ); + }); + + it('with keys', () => { + assert.deepEqual( + transformReply(['0', ['key1', 'key2']]), + { + cursor: 0, + keys: ['key1', 'key2'] + } + ); + }); + }); + + testUtils.testWithClient('client.hScanNoValues', async client => { + assert.deepEqual( + await client.hScanNoValues('key', 0), + { + cursor: 0, + keys: [] + } + ); + + await Promise.all([ + client.hSet('key', 'a', '1'), + client.hSet('key', 'b', '2') + ]); + + assert.deepEqual( + await client.hScanNoValues('key', 0), + { + cursor: 0, + keys: ['a', 'b'] + } + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.ts b/packages/client/lib/commands/HSCAN_NOVALUES.ts new file mode 100644 index 00000000000..37a929754c6 --- /dev/null +++ b/packages/client/lib/commands/HSCAN_NOVALUES.ts @@ -0,0 +1,27 @@ +import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { ScanOptions } from './generic-transformers'; +import { HScanRawReply, transformArguments as transformHScanArguments } from './HSCAN'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HSCAN'; + +export function transformArguments( + key: RedisCommandArgument, + cursor: number, + options?: ScanOptions +): RedisCommandArguments { + const args = transformHScanArguments(key, cursor, options); + args.push('NOVALUES'); + return args; +} + +interface HScanNoValuesReply { + cursor: number; + keys: Array; +} + +export function transformReply([cursor, rawData]: HScanRawReply): HScanNoValuesReply { + return { + cursor: Number(cursor), + keys: rawData + }; +} diff --git a/packages/client/lib/commands/HTTL.spec.ts b/packages/client/lib/commands/HTTL.spec.ts new file mode 100644 index 00000000000..21b8b329a5d --- /dev/null +++ b/packages/client/lib/commands/HTTL.spec.ts @@ -0,0 +1,34 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './HTTL'; +import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; + +describe('HTTL', () => { + testUtils.isVersionGreaterThanHook([7, 4]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HTTL', 'key', 'FIELDS', '1', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2']), + ['HTTL', 'key', 'FIELDS', '2', 'field1', 'field2'] + ); + }); + + }); + + testUtils.testWithClient('hTTL', async client => { + assert.deepEqual( + await client.hTTL('key', 'field1'), + [HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS] + ); + }, { + ...GLOBAL.SERVERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts new file mode 100644 index 00000000000..d3eedd0db0e --- /dev/null +++ b/packages/client/lib/commands/HTTL.ts @@ -0,0 +1,11 @@ +import { RedisCommandArgument } from '.'; +import { pushVerdictArgument } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; +export const IS_READ_ONLY = true; + +export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { + return pushVerdictArgument(['HTTL', key, 'FIELDS'], fields); +} + +export declare function transformReply(): Array | null; diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index 15b934c56ef..771a677f2fb 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -10,7 +10,7 @@ interface XTrimOptions { export function transformArguments( key: RedisCommandArgument, strategy: 'MAXLEN' | 'MINID', - threshold: number, + threshold: number | string, options?: XTrimOptions ): RedisCommandArguments { const args = ['XTRIM', key, strategy]; diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index a9db70c860f..fbbac3e0b71 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -5,7 +5,7 @@ import { promiseTimeout } from './utils'; const utils = new TestUtils({ dockerImageName: 'redis', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '7.2' + defaultDockerVersion: '7.4-rc2' }); export default utils; diff --git a/packages/client/package.json b/packages/client/package.json index 839ce6d8b66..e344edd52c3 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "1.5.14", + "version": "1.6.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index 582b73bf85a..34ca8da289f 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -2,6 +2,8 @@ import { RedisJSON, transformRedisJsonNullReply } from '.'; export const FIRST_KEY_INDEX = 1; +export const IS_READ_ONLY = true; + export function transformArguments(keys: Array, path: string): Array { return [ 'JSON.MGET', diff --git a/packages/json/package.json b/packages/json/package.json index 3a168b7d4b5..ad60cc13c26 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "1.0.6", + "version": "1.0.7", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/search/lib/commands/AGGREGATE.spec.ts b/packages/search/lib/commands/AGGREGATE.spec.ts index d1e4565339a..5b34d7dc16f 100644 --- a/packages/search/lib/commands/AGGREGATE.spec.ts +++ b/packages/search/lib/commands/AGGREGATE.spec.ts @@ -19,6 +19,13 @@ describe('AGGREGATE', () => { ); }); + it('with ADDSCORES', () => { + assert.deepEqual( + transformArguments('index', '*', { ADDSCORES: true }), + ['FT.AGGREGATE', 'index', '*', 'ADDSCORES'] + ); + }); + describe('with LOAD', () => { describe('single', () => { describe('without alias', () => { diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index 950d959243a..0cab9b25d48 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -119,7 +119,8 @@ type LoadField = PropertyName | { } export interface AggregateOptions { - VERBATIM?: true; + VERBATIM?: boolean; + ADDSCORES?: boolean; LOAD?: LoadField | Array; STEPS?: Array; PARAMS?: Params; @@ -150,6 +151,10 @@ export function pushAggregatehOptions( args.push('VERBATIM'); } + if (options?.ADDSCORES) { + args.push('ADDSCORES'); + } + if (options?.LOAD) { args.push('LOAD'); pushArgumentsWithLength(args, () => { @@ -308,4 +313,4 @@ export function transformReply(rawReply: AggregateRawReply): AggregateReply { total: rawReply[0], results }; -} \ No newline at end of file +} diff --git a/packages/search/lib/commands/CREATE.spec.ts b/packages/search/lib/commands/CREATE.spec.ts index 1a0a4f244bd..50c5c011c89 100644 --- a/packages/search/lib/commands/CREATE.spec.ts +++ b/packages/search/lib/commands/CREATE.spec.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import { transformArguments } from './CREATE'; -import { SchemaFieldTypes, SchemaTextFieldPhonetics, RedisSearchLanguages, VectorAlgorithms } from '.'; +import { SchemaFieldTypes, SchemaTextFieldPhonetics, RedisSearchLanguages, VectorAlgorithms, SCHEMA_GEO_SHAPE_COORD_SYSTEM } from '.'; describe('CREATE', () => { describe('transformArguments', () => { @@ -70,6 +70,18 @@ describe('CREATE', () => { ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE'] ); }); + + it('with INDEXEMPTY', () => { + assert.deepEqual( + transformArguments('index', { + field: { + type: SchemaFieldTypes.TEXT, + INDEXEMPTY: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXEMPTY'] + ); + }); }); it('NUMERIC', () => { @@ -148,6 +160,18 @@ describe('CREATE', () => { ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'WITHSUFFIXTRIE'] ); }); + + it('with INDEXEMPTY', () => { + assert.deepEqual( + transformArguments('index', { + field: { + type: SchemaFieldTypes.TAG, + INDEXEMPTY: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'INDEXEMPTY'] + ); + }); }); describe('VECTOR', () => { @@ -196,6 +220,42 @@ describe('CREATE', () => { }); }); + describe('GEOSHAPE', () => { + describe('without options', () => { + it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => { + assert.deepEqual( + transformArguments('index', { + field: SchemaFieldTypes.GEOSHAPE + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE'] + ); + }); + + it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => { + assert.deepEqual( + transformArguments('index', { + field: { + type: SchemaFieldTypes.GEOSHAPE + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE'] + ); + }); + }); + + it('with COORD_SYSTEM', () => { + assert.deepEqual( + transformArguments('index', { + field: { + type: SchemaFieldTypes.GEOSHAPE, + COORD_SYSTEM: SCHEMA_GEO_SHAPE_COORD_SYSTEM.SPHERICAL + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL'] + ); + }); + }); + describe('with generic options', () => { it('with AS', () => { assert.deepEqual( @@ -246,6 +306,18 @@ describe('CREATE', () => { ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX'] ); }); + + it('with INDEXMISSING', () => { + assert.deepEqual( + transformArguments('index', { + field: { + type: SchemaFieldTypes.TEXT, + INDEXMISSING: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXMISSING'] + ); + }); }); }); diff --git a/packages/search/lib/commands/index.ts b/packages/search/lib/commands/index.ts index 75a2b4e380a..f907e1999e6 100644 --- a/packages/search/lib/commands/index.ts +++ b/packages/search/lib/commands/index.ts @@ -185,28 +185,46 @@ export enum SchemaFieldTypes { NUMERIC = 'NUMERIC', GEO = 'GEO', TAG = 'TAG', - VECTOR = 'VECTOR' + VECTOR = 'VECTOR', + GEOSHAPE = 'GEOSHAPE' } - + type CreateSchemaField< T extends SchemaFieldTypes, E = Record > = T | ({ type: T; AS?: string; + INDEXMISSING?: boolean; } & E); +type CommonFieldArguments = { + SORTABLE?: boolean | 'UNF'; + NOINDEX?: boolean; +}; + type CreateSchemaCommonField< T extends SchemaFieldTypes, E = Record > = CreateSchemaField< T, - ({ - SORTABLE?: true | 'UNF'; - NOINDEX?: true; - } & E) + (CommonFieldArguments & E) >; +function pushCommonFieldArguments(args: RedisCommandArguments, fieldOptions: CommonFieldArguments) { + if (fieldOptions.SORTABLE) { + args.push('SORTABLE'); + + if (fieldOptions.SORTABLE === 'UNF') { + args.push('UNF'); + } + } + + if (fieldOptions.NOINDEX) { + args.push('NOINDEX'); + } +} + export enum SchemaTextFieldPhonetics { DM_EN = 'dm:en', DM_FR = 'dm:fr', @@ -219,6 +237,7 @@ type CreateSchemaTextField = CreateSchemaCommonField; type CreateSchemaNumericField = CreateSchemaCommonField; @@ -229,6 +248,7 @@ type CreateSchemaTagField = CreateSchemaCommonField; export enum VectorAlgorithms { @@ -257,6 +277,17 @@ type CreateSchemaHNSWVectorField = CreateSchemaVectorField; +export const SCHEMA_GEO_SHAPE_COORD_SYSTEM = { + SPHERICAL: 'SPHERICAL', + FLAT: 'FLAT' +} as const; + +export type SchemaGeoShapeFieldCoordSystem = typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM[keyof typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM]; + +type CreateSchemaGeoShapeField = CreateSchemaCommonField; + export interface RediSearchSchema { [field: string]: CreateSchemaTextField | @@ -264,7 +295,8 @@ export interface RediSearchSchema { CreateSchemaGeoField | CreateSchemaTagField | CreateSchemaFlatVectorField | - CreateSchemaHNSWVectorField; + CreateSchemaHNSWVectorField | + CreateSchemaGeoShapeField; } export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema) { @@ -300,11 +332,18 @@ export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema args.push('WITHSUFFIXTRIE'); } + pushCommonFieldArguments(args, fieldOptions); + + if (fieldOptions.INDEXEMPTY) { + args.push('INDEXEMPTY'); + } + break; - // case SchemaFieldTypes.NUMERIC: - // case SchemaFieldTypes.GEO: - // break; + case SchemaFieldTypes.NUMERIC: + case SchemaFieldTypes.GEO: + pushCommonFieldArguments(args, fieldOptions); + break; case SchemaFieldTypes.TAG: if (fieldOptions.SEPARATOR) { @@ -319,6 +358,12 @@ export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema args.push('WITHSUFFIXTRIE'); } + pushCommonFieldArguments(args, fieldOptions); + + if (fieldOptions.INDEXEMPTY) { + args.push('INDEXEMPTY'); + } + break; case SchemaFieldTypes.VECTOR: @@ -360,19 +405,20 @@ export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema } }); - continue; // vector fields do not contain SORTABLE and NOINDEX options - } + break; + + case SchemaFieldTypes.GEOSHAPE: + if (fieldOptions.COORD_SYSTEM !== undefined) { + args.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM); + } - if (fieldOptions.SORTABLE) { - args.push('SORTABLE'); + pushCommonFieldArguments(args, fieldOptions); - if (fieldOptions.SORTABLE === 'UNF') { - args.push('UNF'); - } + break; } - if (fieldOptions.NOINDEX) { - args.push('NOINDEX'); + if (fieldOptions.INDEXMISSING) { + args.push('INDEXMISSING'); } } } diff --git a/packages/search/package.json b/packages/search/package.json index 9fb1d0e002d..aaf9bc50f11 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "1.1.6", + "version": "1.2.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/time-series/lib/commands/ADD.spec.ts b/packages/time-series/lib/commands/ADD.spec.ts index 94ad30627f8..07e67c1adec 100644 --- a/packages/time-series/lib/commands/ADD.spec.ts +++ b/packages/time-series/lib/commands/ADD.spec.ts @@ -57,16 +57,26 @@ describe('ADD', () => { ); }); - it('with RETENTION, ENCODING, CHUNK_SIZE, ON_DUPLICATE, LABELS', () => { + it('with IGNORE', () => { + assert.deepEqual( + transformArguments('key', '*', 1, { + IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} + }), + ['TS.ADD', 'key', '*', '1', 'IGNORE', '1', '1'] + ) + }); + + it('with RETENTION, ENCODING, CHUNK_SIZE, ON_DUPLICATE, LABELS, IGNORE', () => { assert.deepEqual( transformArguments('key', '*', 1, { RETENTION: 1, ENCODING: TimeSeriesEncoding.UNCOMPRESSED, CHUNK_SIZE: 1, ON_DUPLICATE: TimeSeriesDuplicatePolicies.BLOCK, - LABELS: { label: 'value' } + LABELS: { label: 'value' }, + IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} }), - ['TS.ADD', 'key', '*', '1', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'ON_DUPLICATE', 'BLOCK', 'LABELS', 'label', 'value'] + ['TS.ADD', 'key', '*', '1', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'ON_DUPLICATE', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] ); }); }); diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts index 1988a964513..3ed185b9b75 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -8,14 +8,21 @@ import { Labels, pushLabelsArgument, Timestamp, + pushIgnoreArgument, } from '.'; +export interface TsIgnoreOptions { + MAX_TIME_DIFF: number; + MAX_VAL_DIFF: number; +} + interface AddOptions { RETENTION?: number; ENCODING?: TimeSeriesEncoding; CHUNK_SIZE?: number; ON_DUPLICATE?: TimeSeriesDuplicatePolicies; LABELS?: Labels; + IGNORE?: TsIgnoreOptions; } export const FIRST_KEY_INDEX = 1; @@ -40,6 +47,8 @@ export function transformArguments(key: string, timestamp: Timestamp, value: num pushLabelsArgument(args, options?.LABELS); + pushIgnoreArgument(args, options?.IGNORE); + return args; } diff --git a/packages/time-series/lib/commands/ALTER.spec.ts b/packages/time-series/lib/commands/ALTER.spec.ts index cd066533aa4..7add3eeec3a 100644 --- a/packages/time-series/lib/commands/ALTER.spec.ts +++ b/packages/time-series/lib/commands/ALTER.spec.ts @@ -48,15 +48,25 @@ describe('ALTER', () => { ); }); - it('with RETENTION, CHUNK_SIZE, DUPLICATE_POLICY, LABELS', () => { + it('with IGNORE with MAX_TIME_DIFF', () => { + assert.deepEqual( + transformArguments('key', { + IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} + }), + ['TS.ALTER', 'key', 'IGNORE', '1', '1'] + ) + }); + + it('with RETENTION, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { assert.deepEqual( transformArguments('key', { RETENTION: 1, CHUNK_SIZE: 1, DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, - LABELS: { label: 'value' } + LABELS: { label: 'value' }, + IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} }), - ['TS.ALTER', 'key', 'RETENTION', '1', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value'] + ['TS.ALTER', 'key', 'RETENTION', '1', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] ); }); }); diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts index 7b9e1e774c6..576153a0cca 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -1,4 +1,5 @@ -import { pushRetentionArgument, Labels, pushLabelsArgument, TimeSeriesDuplicatePolicies, pushChunkSizeArgument, pushDuplicatePolicy } from '.'; +import { pushRetentionArgument, Labels, pushLabelsArgument, TimeSeriesDuplicatePolicies, pushChunkSizeArgument, pushDuplicatePolicy, pushIgnoreArgument } from '.'; +import { TsIgnoreOptions } from './ADD'; export const FIRST_KEY_INDEX = 1; @@ -7,6 +8,7 @@ interface AlterOptions { CHUNK_SIZE?: number; DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies; LABELS?: Labels; + IGNORE?: TsIgnoreOptions; } export function transformArguments(key: string, options?: AlterOptions): Array { @@ -20,6 +22,8 @@ export function transformArguments(key: string, options?: AlterOptions): Array { ['TS.CREATE', 'key', 'LABELS', 'label', 'value'] ); }); + + it('with IGNORE with MAX_TIME_DIFF', () => { + assert.deepEqual( + transformArguments('key', { + IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} + }), + ['TS.CREATE', 'key', 'IGNORE', '1', '1'] + ) + }); - it('with RETENTION, ENCODING, CHUNK_SIZE, DUPLICATE_POLICY, LABELS', () => { + it('with RETENTION, ENCODING, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { assert.deepEqual( transformArguments('key', { RETENTION: 1, ENCODING: TimeSeriesEncoding.UNCOMPRESSED, CHUNK_SIZE: 1, DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, - LABELS: { label: 'value' } + LABELS: { label: 'value' }, + IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} }), - ['TS.CREATE', 'key', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value'] + ['TS.CREATE', 'key', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] ); }); }); diff --git a/packages/time-series/lib/commands/CREATE.ts b/packages/time-series/lib/commands/CREATE.ts index a360950feff..a84d4b5f9fb 100644 --- a/packages/time-series/lib/commands/CREATE.ts +++ b/packages/time-series/lib/commands/CREATE.ts @@ -6,8 +6,10 @@ import { TimeSeriesDuplicatePolicies, Labels, pushLabelsArgument, - pushDuplicatePolicy + pushDuplicatePolicy, + pushIgnoreArgument } from '.'; +import { TsIgnoreOptions } from './ADD'; export const FIRST_KEY_INDEX = 1; @@ -17,6 +19,7 @@ interface CreateOptions { CHUNK_SIZE?: number; DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies; LABELS?: Labels; + IGNORE?: TsIgnoreOptions; } export function transformArguments(key: string, options?: CreateOptions): Array { @@ -32,6 +35,8 @@ export function transformArguments(key: string, options?: CreateOptions): Array< pushLabelsArgument(args, options?.LABELS); + pushIgnoreArgument(args, options?.IGNORE); + return args; } diff --git a/packages/time-series/lib/commands/index.ts b/packages/time-series/lib/commands/index.ts index 356b0416648..ca382498060 100644 --- a/packages/time-series/lib/commands/index.ts +++ b/packages/time-series/lib/commands/index.ts @@ -127,6 +127,12 @@ export function transformTimestampArgument(timestamp: Timestamp): string { ).toString(); } +export function pushIgnoreArgument(args: RedisCommandArguments, ignore?: ADD.TsIgnoreOptions) { + if (ignore !== undefined) { + args.push('IGNORE', ignore.MAX_TIME_DIFF.toString(), ignore.MAX_VAL_DIFF.toString()); + } +} + export function pushRetentionArgument(args: RedisCommandArguments, retention?: number): RedisCommandArguments { if (retention !== undefined) { args.push( diff --git a/packages/time-series/package.json b/packages/time-series/package.json index ee3c7526192..65ee1e99c23 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "1.0.5", + "version": "1.1.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts",