1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

v4.0.0-rc.4 (#1723)

* update workflows & README

* add .deepsource.toml

* fix client.quit, add error events on cluster, fix some "deepsource.io" warnings

* Release 4.0.0-rc.1

* add cluster.duplicate, add some tests

* fix #1650 - add support for Buffer in some commands, add GET_BUFFER command

* fix GET and GET_BUFFER return type

* update FAQ

* Update invalid code example in README.md (#1654)

* Update invalid code example in README.md

* Update README.md

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* fix #1652

* ref #1653 - better types

* better types

* fix 54124793ad

* Update GEOSEARCHSTORE.spec.ts

* fix #1660 - add support for client.HSET('key', 'field', 'value')

* upgrade dependencies, update README

* fix #1659 - add support for db-number in client options url

* fix README, remove unused import, downgrade typedoc & typedoc-plugin-markdown

* update client-configurations.md

* fix README

* add CLUSTER_SLOTS, add some tests

* fix "createClient with url" test with redis 5

* remove unused imports

* Release 4.0.0-rc.2

* add missing semicolon

* replace empty "transformReply" functions with typescript "declare"

* fix EVAL & EVALSHA, add some tests, npm update

* fix #1665 - add ZRANGEBYLEX, ZRANGEBYSCORE, ZRANGEBYSCORE_WITHSCORES

* new issue templates

* add all COMMAND commands

* run COMMAND & COMMAND INFO tests only on redis >6

* Create SECURITY.md

* fix #1671 - add support for all client configurations in cluster

* ref #1671 - add support for defaults

* remove some commands from cluster, npm update, clean code,

* lock benny version

* fix #1674 - remove `isolationPoolOptions` when creating isolated connection

* increase test coverage

* update .npmignore

* Release 4.0.0-rc.3

* fix README

* remove whitespace from LICENSE

* use "export { x as y }" instead of import & const

* move from "NodeRedis" to "Redis"

* fix #1676

* update comments

* Auth before select database (#1679)

* Auth before select database

* fix #1681

Co-authored-by: leibale <leibale1998@gmail.com>

* Adds connect-as-acl-user example. (#1684)

* Adds connect-as-acl-user example.

* Adds blank line at end.

* Set to private.

* Adds examples folder to npmignore.

* Adds Apple .DS_Store file to .gitignore (#1685)

* Adds Apple .DS_Store file.

* Add .DS_Store to .npmignore too

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* move examples

* clean some tests

* clean code

* Adds examples table of contents and contribution guidelines. (#1686)

* Updated examples to use named functions. (#1687)

* Updated examples to user named functions.

* Update README.md

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* update docs, add 6.0.x to the tests matrix, add eslint, npm update, fix some commands, fix some types

Co-authored-by: Simon Prickett <simon@crudworks.org>

* fix tests with redis 6.0.x

* fix ACL GETUSER test

* fix client.quit and client.disconnect

* fix ACL GETUSER

* Adds TypeScript note and corrects a typo.

* Fixes a bug in the Scan Iterator section. (#1694)

* Made examples use local version.

* Add `lua-multi-incr.js` example (#1692)

Also fix syntax error in the lua example in the README

Closes #1689.

* Add(examples): Create an example for blPop & lPush (#1696)

* Add(examples): Create an example for blPop & lPush

Signed-off-by: Aditya Rastogi <adit.rastogi2014@gmail.com>

* Update(examples): fix case, add timeout, update readme

Signed-off-by: Aditya Rastogi <adit.rastogi2014@gmail.com>

Closes #1693.

* Add command-with-modifiers.js example (#1695)

* Adds TypeScript note and corrects a typo.

* Adds command-with-modifiers example. (redis#1688)

* Adds command-with-modifiers example. (redis#1688)

* Adds command-with-modifiers example. (redis#1688)

* Removed callbacks.

Co-authored-by: Simon Prickett <simon@redislabs.com>

Closes #1688.

* Issue # 1697 FIX - creates an example script that shows how to use the SSCAN iterator (#1699)

* #1697 fix for set scan example

* adds the js file

* adds comment

* Minor layout and comment adjustment.

Co-authored-by: srawat2 <shashank19aug>
Co-authored-by: Simon Prickett <simon@redislabs.com>

Closes #1697.

* fix #1706 - HSET return type should be number

* use dockers for tests, fix some bugs

* increase dockers timeout to 30s

* release drafter (#1683)

* release drafter

* fixing contributors

* use dockers for tests, use npm workspaces, add rejson & redisearch modules, fix some bugs

* fix #1712 - fix LINDEX return type

* uncomment TIME tests

* use codecov

* fix tests.yml

* uncomment "should handle live resharding" test

* fix #1714 - update README(s)

* add package-lock.json

* update CONTRIBUTING.md

* update examples

* uncomment some tests

* fix test-utils

* move "all-in-one" to root folder

* fix tests workflow

* fix bug in cluster slots, enhance live resharding test

* fix live resharding test

* fix #1707 - handle number arguments in legacy mode

* Add rejectedUnauthorized and other TLS options (#1708)

* Update socket.ts

* fix #1716 - decode username and password from url

* fix some Z (sorted list) commands, increase commands test coverage

* remove empty lines

* fix 'Scenario' typo (#1720)

* update readmes, add createCluster to the `redis` package

* add .release-it.json files, update some md files

* run tests on pull requests too

* Support esModuleInterop set to false. (#1717)

* Support esModuleInterop set to false.

When testing the upcoming 4.x release, we got a bunch of typescript
errors emitted from this project.

We quickly realized this is because the library uses the esModuleInterop
flag. This makes some imports _slightly_ easier to write, but it comes
at a cost: it forces any application or library using this library to
*also* have esModuleInterop on.

The `esModuleInterop` flag is a bit of a holdover from an earlier time,
and I would not recommend using it in libraries. The main issue is that
if it's set to true, you are forcing any users of the library to also
have `esModuleInterop`, where if you keep have it set to `false` (the
default), you leave the decision to the user.

This change should have no rammifications to users with
`esModuleInterop` on, but it will enable support for those that have it
off.

This is especially good for library authors such as myself, because I
would also like to keep this flag off to not force *my* users into this
feature.

* All tests now pass!

* Move @types/redis-parser into client sub-package

and removed a comma

* npm update, remove html from readme

* add tests and licence badges

* update changelog.md

* update .npmignore and .release-it.json

* update .release-it.json

* Release client@1.0.0-rc.0

* revert d32f1edf8a

* fix .npmignore

* replace @redis with @node-redis

* Release client@1.0.0-rc.0

* update json & search version

* Release json@1.0.0-rc.0

* Release search@1.0.0-rc.0

* update dependencies

* Release redis@4.0.0-rc.4

Co-authored-by: Richard Samuelsson <noobtoothfairy@gmail.com>
Co-authored-by: mustard <mhqnwt@gmail.com>
Co-authored-by: Simon Prickett <simon@redislabs.com>
Co-authored-by: Simon Prickett <simon@crudworks.org>
Co-authored-by: Suze Shardlow <SuzeShardlow@users.noreply.github.com>
Co-authored-by: Joshua T <buildingsomethingfun@gmail.com>
Co-authored-by: Aditya Rastogi <adit.rastogi2014@gmail.com>
Co-authored-by: Rohan Kumar <rohan.kr20@gmail.com>
Co-authored-by: Kalki <shashank.kviit@gmail.com>
Co-authored-by: Chayim <chayim@users.noreply.github.com>
Co-authored-by: Da-Jin Chu <dajinchu@gmail.com>
Co-authored-by: Henrique Corrêa <75134774+HeCorr@users.noreply.github.com>
Co-authored-by: Evert Pot <me@evertpot.com>
This commit is contained in:
Leibale Eidelman
2021-11-16 02:48:20 -05:00
committed by GitHub
parent 199285aa71
commit eed479778f
705 changed files with 9959 additions and 4349 deletions

View File

@@ -0,0 +1,6 @@
.nyc_output/
coverage/
lib/
.nycrc.json
.release-it.json
tsconfig.json

View File

@@ -0,0 +1,4 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"exclude": ["**/*.spec.ts", "lib/test-utils.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"git": {
"tagName": "search@${version}",
"commitMessage": "Release ${tagName}",
"tagAnnotation": "Release ${tagName}"
},
"npm": {
"publishArgs": ["--access", "public"]
}
}

View File

@@ -0,0 +1,2 @@
# @node-redis/search
The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo.

View File

@@ -0,0 +1,482 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { AggregateGroupByReducers, AggregateSteps, transformArguments } from './AGGREGATE';
import { SchemaFieldTypes } from './CREATE';
describe('AGGREGATE', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', '*'),
['FT.AGGREGATE', 'index', '*']
);
});
it('with VERBATIM', () => {
assert.deepEqual(
transformArguments('index', '*', { VERBATIM: true }),
['FT.AGGREGATE', 'index', '*', 'VERBATIM']
);
});
describe('with LOAD', () => {
describe('single', () => {
describe('without alias', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', '*', { LOAD: '@property' }),
['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property']
);
});
it('{ identifier: string }', () => {
assert.deepEqual(
transformArguments('index', '*', {
LOAD: {
identifier: '@property'
}
}),
['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property']
);
});
});
it('with alias', () => {
assert.deepEqual(
transformArguments('index', '*', {
LOAD: {
identifier: '@property',
AS: 'alias'
}
}),
['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias']
);
});
});
it('multiple', () => {
assert.deepEqual(
transformArguments('index', '*', { LOAD: ['@1', '@2'] }),
['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2']
);
});
});
describe('with STEPS', () => {
describe('GROUPBY', () => {
describe('COUNT', () => {
describe('without properties', () => {
it('without alias', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.COUNT
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0']
);
});
it('with alias', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.COUNT,
AS: 'count'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count']
);
});
});
describe('with properties', () => {
it('single', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
properties: '@property',
REDUCE: {
type: AggregateGroupByReducers.COUNT
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
properties: ['@1', '@2'],
REDUCE: {
type: AggregateGroupByReducers.COUNT
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0']
);
});
});
});
it('COUNT_DISTINCT', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.COUNT_DISTINCT,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property']
);
});
it('COUNT_DISTINCTISH', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.COUNT_DISTINCTISH,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property']
);
});
it('SUM', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.SUM,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property']
);
});
it('MIN', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.MIN,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property']
);
});
it('MAX', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.MAX,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property']
);
});
it('AVG', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.AVG,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property']
);
});
it('STDDEV', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.STDDEV,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property']
);
});
it('QUANTILE', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.QUANTILE,
property: '@property',
quantile: 0.5
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5']
);
});
it('TO_LIST', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.TO_LIST,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property']
);
});
describe('FIRST_VALUE', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.FIRST_VALUE,
property: '@property'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property']
);
});
describe('with BY', () => {
describe('without direction', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.FIRST_VALUE,
property: '@property',
BY: '@by'
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by']
);
});
it('{ property: string }', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.FIRST_VALUE,
property: '@property',
BY: {
property: '@by'
}
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by']
);
});
});
it('with direction', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.FIRST_VALUE,
property: '@property',
BY: {
property: '@by',
direction: 'ASC'
}
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '4', '@property', 'BY', '@by', 'ASC']
);
});
});
});
it('RANDOM_SAMPLE', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: {
type: AggregateGroupByReducers.RANDOM_SAMPLE,
property: '@property',
sampleSize: 1
}
}]
}),
['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1']
);
});
});
describe('SORTBY', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.SORTBY,
BY: '@by'
}]
}),
['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.SORTBY,
BY: ['@1', '@2']
}]
}),
['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2']
);
});
it('with MAX', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.SORTBY,
BY: '@by',
MAX: 1
}]
}),
['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by', 'MAX', '1']
);
});
});
describe('APPLY', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.APPLY,
expression: '@field + 1',
AS: 'as'
}]
}),
['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as']
);
});
describe('LIMIT', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.LIMIT,
from: 0,
size: 1
}]
}),
['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1']
);
});
describe('FILTER', () => {
assert.deepEqual(
transformArguments('index', '*', {
STEPS: [{
type: AggregateSteps.FILTER,
expression: '@field != ""'
}]
}),
['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""']
);
});
});
});
testUtils.testWithClient('client.ft.aggregate', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
}),
client.hSet('1', 'field', '1'),
client.hSet('2', 'field', '2')
]);
assert.deepEqual(
await client.ft.aggregate('index', '*', {
STEPS: [{
type: AggregateSteps.GROUPBY,
REDUCE: [{
type: AggregateGroupByReducers.SUM,
property: '@field',
AS: 'sum'
}, {
type: AggregateGroupByReducers.AVG,
property: '@field',
AS: 'avg'
}]
}]
}),
{
total: 1,
results: [
Object.create(null, {
sum: {
value: '3',
configurable: true,
enumerable: true
},
avg: {
value: '1.5',
configurable: true,
enumerable: true
}
})
]
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,283 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { pushVerdictArgument, transformReplyTuples, TuplesObject } from '@node-redis/client/dist/lib/commands/generic-transformers';
import { PropertyName, pushArgumentsWithLength, pushSortByArguments, SortByOptions } from '.';
export enum AggregateSteps {
GROUPBY = 'GROUPBY',
SORTBY = 'SORTBY',
APPLY = 'APPLY',
LIMIT = 'LIMIT',
FILTER = 'FILTER'
}
interface AggregateStep<T extends AggregateSteps> {
type: T;
}
export enum AggregateGroupByReducers {
COUNT = 'COUNT',
COUNT_DISTINCT = 'COUNT_DISTINCT',
COUNT_DISTINCTISH = 'COUNT_DISTINCTISH',
SUM = 'SUM',
MIN = 'MIN',
MAX = 'MAX',
AVG = 'AVG',
STDDEV = 'STDDEV',
QUANTILE = 'QUANTILE',
TOLIST = 'TOLIST',
TO_LIST = 'TOLIST',
FIRST_VALUE = 'FIRST_VALUE',
RANDOM_SAMPLE = 'RANDOM_SAMPLE'
}
interface GroupByReducer<T extends AggregateGroupByReducers> {
type: T;
AS?: string;
}
type CountReducer = GroupByReducer<AggregateGroupByReducers.COUNT>;
interface CountDistinctReducer extends GroupByReducer<AggregateGroupByReducers.COUNT_DISTINCT> {
property: PropertyName;
}
interface CountDistinctishReducer extends GroupByReducer<AggregateGroupByReducers.COUNT_DISTINCTISH> {
property: PropertyName;
}
interface SumReducer extends GroupByReducer<AggregateGroupByReducers.SUM> {
property: PropertyName;
}
interface MinReducer extends GroupByReducer<AggregateGroupByReducers.MIN> {
property: PropertyName;
}
interface MaxReducer extends GroupByReducer<AggregateGroupByReducers.MAX> {
property: PropertyName;
}
interface AvgReducer extends GroupByReducer<AggregateGroupByReducers.AVG> {
property: PropertyName;
}
interface StdDevReducer extends GroupByReducer<AggregateGroupByReducers.STDDEV> {
property: PropertyName;
}
interface QuantileReducer extends GroupByReducer<AggregateGroupByReducers.QUANTILE> {
property: PropertyName;
quantile: number;
}
interface ToListReducer extends GroupByReducer<AggregateGroupByReducers.TOLIST> {
property: PropertyName;
}
interface FirstValueReducer extends GroupByReducer<AggregateGroupByReducers.FIRST_VALUE> {
property: PropertyName;
BY?: PropertyName | {
property: PropertyName;
direction?: 'ASC' | 'DESC';
};
}
interface RandomSampleReducer extends GroupByReducer<AggregateGroupByReducers.RANDOM_SAMPLE> {
property: PropertyName;
sampleSize: number;
}
type GroupByReducers = CountReducer | CountDistinctReducer | CountDistinctishReducer | SumReducer | MinReducer | MaxReducer | AvgReducer | StdDevReducer | QuantileReducer | ToListReducer | FirstValueReducer | RandomSampleReducer;
interface GroupByStep extends AggregateStep<AggregateSteps.GROUPBY> {
properties?: PropertyName | Array<PropertyName>;
REDUCE: GroupByReducers | Array<GroupByReducers>;
}
interface SortStep extends AggregateStep<AggregateSteps.SORTBY> {
BY: SortByOptions | Array<SortByOptions>;
MAX?: number;
}
interface ApplyStep extends AggregateStep<AggregateSteps.APPLY> {
expression: string;
AS: string;
}
interface LimitStep extends AggregateStep<AggregateSteps.LIMIT> {
from: number;
size: number;
}
interface FilterStep extends AggregateStep<AggregateSteps.FILTER> {
expression: string;
}
type LoadField = PropertyName | {
identifier: PropertyName;
AS?: string;
}
interface AggregateOptions {
VERBATIM?: true;
LOAD?: LoadField | Array<LoadField>;
STEPS?: Array<GroupByStep | SortStep | ApplyStep | LimitStep | FilterStep>;
}
export function transformArguments(index: string, query: string, options?: AggregateOptions): RedisCommandArguments {
const args = ['FT.AGGREGATE', index, query];
if (options?.VERBATIM) {
args.push('VERBATIM');
}
if (options?.LOAD) {
args.push('LOAD');
pushArgumentsWithLength(args, () => {
if (Array.isArray(options.LOAD)) {
for (const load of options.LOAD) {
pushLoadField(args, load);
}
} else {
pushLoadField(args, options.LOAD!);
}
});
}
if (options?.STEPS) {
for (const step of options.STEPS) {
switch (step.type) {
case AggregateSteps.GROUPBY:
args.push('GROUPBY');
if (!step.properties) {
args.push('0');
} else {
pushVerdictArgument(args, step.properties);
}
if (Array.isArray(step.REDUCE)) {
for (const reducer of step.REDUCE) {
pushGroupByReducer(args, reducer);
}
} else {
pushGroupByReducer(args, step.REDUCE);
}
break;
case AggregateSteps.SORTBY:
pushSortByArguments(args, 'SORTBY', step.BY);
if (step.MAX) {
args.push('MAX', step.MAX.toString());
}
break;
case AggregateSteps.APPLY:
args.push('APPLY', step.expression, 'AS', step.AS);
break;
case AggregateSteps.LIMIT:
args.push('LIMIT', step.from.toString(), step.size.toString());
break;
case AggregateSteps.FILTER:
args.push('FILTER', step.expression);
break;
}
}
}
return args;
}
function pushLoadField(args: RedisCommandArguments, toLoad: LoadField): void {
if (typeof toLoad === 'string') {
args.push(toLoad);
} else {
args.push(toLoad.identifier);
if (toLoad.AS) {
args.push('AS', toLoad.AS);
}
}
}
function pushGroupByReducer(args: RedisCommandArguments, reducer: GroupByReducers): void {
args.push('REDUCE', reducer.type);
switch (reducer.type) {
case AggregateGroupByReducers.COUNT:
args.push('0');
break;
case AggregateGroupByReducers.COUNT_DISTINCT:
case AggregateGroupByReducers.COUNT_DISTINCTISH:
case AggregateGroupByReducers.SUM:
case AggregateGroupByReducers.MIN:
case AggregateGroupByReducers.MAX:
case AggregateGroupByReducers.AVG:
case AggregateGroupByReducers.STDDEV:
case AggregateGroupByReducers.TOLIST:
args.push('1', reducer.property);
break;
case AggregateGroupByReducers.QUANTILE:
args.push('2', reducer.property, reducer.quantile.toString());
break;
case AggregateGroupByReducers.FIRST_VALUE: {
pushArgumentsWithLength(args, () => {
args.push(reducer.property);
if (reducer.BY) {
args.push('BY');
if (typeof reducer.BY === 'string') {
args.push(reducer.BY);
} else {
args.push(reducer.BY.property);
if (reducer.BY.direction) {
args.push(reducer.BY.direction);
}
}
}
});
break;
}
case AggregateGroupByReducers.RANDOM_SAMPLE:
args.push('2', reducer.property, reducer.sampleSize.toString());
break;
}
if (reducer.AS) {
args.push('AS', reducer.AS);
}
}
type AggregateRawReply = [
total: number,
...results: Array<Array<string>>
];
interface AggregateReply {
total: number;
results: Array<TuplesObject>;
}
export function transformReply(rawReply: AggregateRawReply): AggregateReply {
const results: Array<TuplesObject> = [];
for (let i = 1; i < rawReply.length; i++) {
results.push(
transformReplyTuples(rawReply[i] as Array<string>)
);
}
return {
total: rawReply[0],
results
};
}

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ALIASADD';
describe('ALIASADD', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('alias', 'index'),
['FT.ALIASADD', 'alias', 'index']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(name: string, index: string): Array<string> {
return ['FT.ALIASADD', name, index];
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ALIASDEL';
describe('ALIASDEL', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('alias', 'index'),
['FT.ALIASDEL', 'alias', 'index']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(name: string, index: string): Array<string> {
return ['FT.ALIASDEL', name, index];
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ALIASUPDATE';
describe('ALIASUPDATE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('alias', 'index'),
['FT.ALIASUPDATE', 'alias', 'index']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(name: string, index: string): Array<string> {
return ['FT.ALIASUPDATE', name, index];
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,25 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CONFIG_GET';
describe('CONFIG GET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('TIMEOUT'),
['FT.CONFIG', 'GET', 'TIMEOUT']
);
});
testUtils.testWithClient('client.ft.configGet', async client => {
assert.deepEqual(
await client.ft.configGet('TIMEOUT'),
Object.create(null, {
TIMEOUT: {
value: '500',
configurable: true,
enumerable: true
}
})
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,16 @@
export function transformArguments(option: string) {
return ['FT.CONFIG', 'GET', option];
}
interface ConfigGetReply {
[option: string]: string | null;
}
export function transformReply(rawReply: Array<[string, string | null]>): ConfigGetReply {
const transformedReply: ConfigGetReply = Object.create(null);
for (const [key, value] of rawReply) {
transformedReply[key] = value;
}
return transformedReply;
}

View File

@@ -0,0 +1,12 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CONFIG_SET';
describe('CONFIG SET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('TIMEOUT', '500'),
['FT.CONFIG', 'SET', 'TIMEOUT', '500']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(option: string, value: string): Array<string> {
return ['FT.CONFIG', 'SET', option, value];
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,350 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes, SchemaTextFieldPhonetics, transformArguments } from './CREATE';
import { RedisSearchLanguages } from '.';
describe('CREATE', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('index', {}),
['FT.CREATE', 'index', 'SCHEMA']
);
});
describe('with fields', () => {
describe('TEXT', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.TEXT
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT']
);
});
it('with NOSTEM', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
NOSTEM: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOSTEM']
);
});
it('with WEIGHT', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
WEIGHT: 1
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WEIGHT', '1']
);
});
it('with PHONETIC', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
PHONETIC: SchemaTextFieldPhonetics.DM_EN
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'PHONETIC', SchemaTextFieldPhonetics.DM_EN]
);
});
});
it('NUMERIC', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.NUMERIC
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC']
);
});
it('GEO', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.GEO
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO']
);
});
describe('TAG', () => {
describe('without options', () => {
it('SchemaFieldTypes.TAG', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.TAG
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
);
});
it('{ type: SchemaFieldTypes.TAG }', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
);
});
});
it('with SEPERATOR', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG,
SEPERATOR: 'seperator'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'SEPERATOR', 'seperator']
);
});
it('with CASESENSITIVE', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG,
CASESENSITIVE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'CASESENSITIVE']
);
});
});
describe('with generic options', () => {
it('with AS', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
AS: 'as'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT']
);
});
describe('with SORTABLE', () => {
it('true', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
SORTABLE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE']
);
});
it('UNF', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
SORTABLE: 'UNF'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF']
);
});
});
it('with NOINDEX', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
NOINDEX: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX']
);
});
});
});
it('with ON', () => {
assert.deepEqual(
transformArguments('index', {}, {
ON: 'HASH'
}),
['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA']
);
});
describe('with PREFIX', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', {}, {
PREFIX: 'prefix'
}),
['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', {}, {
PREFIX: ['1', '2']
}),
['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA']
);
});
});
it('with FILTER', () => {
assert.deepEqual(
transformArguments('index', {}, {
FILTER: '@field != ""'
}),
['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA']
);
});
it('with LANGUAGE', () => {
assert.deepEqual(
transformArguments('index', {}, {
LANGUAGE: RedisSearchLanguages.ARABIC
}),
['FT.CREATE', 'index', 'LANGUAGE', RedisSearchLanguages.ARABIC, 'SCHEMA']
);
});
it('with LANGUAGE_FIELD', () => {
assert.deepEqual(
transformArguments('index', {}, {
LANGUAGE_FIELD: '@field'
}),
['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA']
);
});
it('with SCORE', () => {
assert.deepEqual(
transformArguments('index', {}, {
SCORE: 1
}),
['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA']
);
});
it('with SCORE_FIELD', () => {
assert.deepEqual(
transformArguments('index', {}, {
SCORE_FIELD: '@field'
}),
['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA']
);
});
it('with MAXTEXTFIELDS', () => {
assert.deepEqual(
transformArguments('index', {}, {
MAXTEXTFIELDS: true
}),
['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA']
);
});
it('with TEMPORARY', () => {
assert.deepEqual(
transformArguments('index', {}, {
TEMPORARY: 1
}),
['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA']
);
});
it('with NOOFFSETS', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOOFFSETS: true
}),
['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA']
);
});
it('with NOHL', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOHL: true
}),
['FT.CREATE', 'index', 'NOHL', 'SCHEMA']
);
});
it('with NOFIELDS', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOFIELDS: true
}),
['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA']
);
});
it('with NOFREQS', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOFREQS: true
}),
['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA']
);
});
it('with SKIPINITIALSCAN', () => {
assert.deepEqual(
transformArguments('index', {}, {
SKIPINITIALSCAN: true
}),
['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA']
);
});
describe('with STOPWORDS', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', {}, {
STOPWORDS: 'stopword'
}),
['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', {}, {
STOPWORDS: ['1', '2']
}),
['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA']
);
});
});
});
testUtils.testWithClient('client.ft.create', async client => {
assert.equal(
await client.ft.create('index', {
field: SchemaFieldTypes.TEXT // TODO: shouldn't be mandatory
}),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,194 @@
import { pushOptionalVerdictArgument } from '@node-redis/client/dist/lib/commands/generic-transformers';
import { RedisSearchLanguages, PropertyName } from '.';
export enum SchemaFieldTypes {
TEXT = 'TEXT',
NUMERIC = 'NUMERIC',
GEO = 'GEO',
TAG = 'TAG'
}
type CreateSchemaField<T extends SchemaFieldTypes, E = Record<string, never>> = T | ({
type: T;
AS?: string;
SORTABLE?: true | 'UNF';
NOINDEX?: true;
} & E);
export enum SchemaTextFieldPhonetics {
DM_EN = 'dm:en',
DM_FR = 'dm:fr',
FM_PT = 'dm:pt',
DM_ES = 'dm:es'
}
type CreateSchemaTextField = CreateSchemaField<SchemaFieldTypes.TEXT, {
NOSTEM?: true;
WEIGHT?: number;
PHONETIC?: SchemaTextFieldPhonetics;
}>;
type CreateSchemaNumericField = CreateSchemaField<SchemaFieldTypes.NUMERIC>;
type CreateSchemaGeoField = CreateSchemaField<SchemaFieldTypes.GEO>;
type CreateSchemaTagField = CreateSchemaField<SchemaFieldTypes.TAG, {
SEPERATOR?: string;
CASESENSITIVE?: true;
}>;
interface CreateSchema {
[field: string]:
CreateSchemaTextField |
CreateSchemaNumericField |
CreateSchemaGeoField |
CreateSchemaTagField
}
interface CreateOptions {
ON?: 'HASH' | 'JSON';
PREFIX?: string | Array<string>;
FILTER?: string;
LANGUAGE?: RedisSearchLanguages;
LANGUAGE_FIELD?: PropertyName;
SCORE?: number;
SCORE_FIELD?: PropertyName;
// PAYLOAD_FIELD?: string;
MAXTEXTFIELDS?: true;
TEMPORARY?: number;
NOOFFSETS?: true;
NOHL?: true;
NOFIELDS?: true;
NOFREQS?: true;
SKIPINITIALSCAN?: true;
STOPWORDS?: string | Array<string>;
}
export function transformArguments(index: string, schema: CreateSchema, options?: CreateOptions): Array<string> {
const args = ['FT.CREATE', index];
if (options?.ON) {
args.push('ON', options.ON);
}
pushOptionalVerdictArgument(args, 'PREFIX', options?.PREFIX);
if (options?.FILTER) {
args.push('FILTER', options.FILTER);
}
if (options?.LANGUAGE) {
args.push('LANGUAGE', options.LANGUAGE);
}
if (options?.LANGUAGE_FIELD) {
args.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD);
}
if (options?.SCORE) {
args.push('SCORE', options.SCORE.toString());
}
if (options?.SCORE_FIELD) {
args.push('SCORE_FIELD', options.SCORE_FIELD);
}
// if (options?.PAYLOAD_FIELD) {
// args.push('PAYLOAD_FIELD', options.PAYLOAD_FIELD);
// }
if (options?.MAXTEXTFIELDS) {
args.push('MAXTEXTFIELDS');
}
if (options?.TEMPORARY) {
args.push('TEMPORARY', options.TEMPORARY.toString());
}
if (options?.NOOFFSETS) {
args.push('NOOFFSETS');
}
if (options?.NOHL) {
args.push('NOHL');
}
if (options?.NOFIELDS) {
args.push('NOFIELDS');
}
if (options?.NOFREQS) {
args.push('NOFREQS');
}
if (options?.SKIPINITIALSCAN) {
args.push('SKIPINITIALSCAN');
}
pushOptionalVerdictArgument(args, 'STOPWORDS', options?.STOPWORDS);
args.push('SCHEMA');
for (const [field, fieldOptions] of Object.entries(schema)) {
args.push(field);
if (typeof fieldOptions === 'string') {
args.push(fieldOptions);
continue;
}
if (fieldOptions.AS) {
args.push('AS', fieldOptions.AS);
}
args.push(fieldOptions.type);
switch (fieldOptions.type) {
case 'TEXT':
if (fieldOptions.NOSTEM) {
args.push('NOSTEM');
}
if (fieldOptions.WEIGHT) {
args.push('WEIGHT', fieldOptions.WEIGHT.toString());
}
if (fieldOptions.PHONETIC) {
args.push('PHONETIC', fieldOptions.PHONETIC);
}
break;
// case 'NUMERIC':
// case 'GEO':
// break;
case 'TAG':
if (fieldOptions.SEPERATOR) {
args.push('SEPERATOR', fieldOptions.SEPERATOR);
}
if (fieldOptions.CASESENSITIVE) {
args.push('CASESENSITIVE');
}
break;
}
if (fieldOptions.SORTABLE) {
args.push('SORTABLE');
if (fieldOptions.SORTABLE === 'UNF') {
args.push('UNF');
}
}
if (fieldOptions.NOINDEX) {
args.push('NOINDEX');
}
}
return args;
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DICTADD';
describe('DICTADD', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('dictionary', 'term'),
['FT.DICTADD', 'dictionary', 'term']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('dictionary', ['1', '2']),
['FT.DICTADD', 'dictionary', '1', '2']
);
});
});
testUtils.testWithClient('client.ft.dictAdd', async client => {
assert.equal(
await client.ft.dictAdd('dictionary', 'term'),
1
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,8 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@node-redis/client/dist/lib/commands/generic-transformers';
export function transformArguments(dictionary: string, term: string | Array<string>): RedisCommandArguments {
return pushVerdictArguments(['FT.DICTADD', dictionary], term);
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DICTDEL';
describe('DICTDEL', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('dictionary', 'term'),
['FT.DICTDEL', 'dictionary', 'term']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('dictionary', ['1', '2']),
['FT.DICTDEL', 'dictionary', '1', '2']
);
});
});
testUtils.testWithClient('client.ft.dictDel', async client => {
assert.equal(
await client.ft.dictDel('dictionary', 'term'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,8 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@node-redis/client/dist/lib/commands/generic-transformers';
export function transformArguments(dictionary: string, term: string | Array<string>): RedisCommandArguments {
return pushVerdictArguments(['FT.DICTDEL', dictionary], term);
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,21 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DICTDUMP';
describe('DICTDUMP', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('dictionary'),
['FT.DICTDUMP', 'dictionary']
);
});
testUtils.testWithClient('client.ft.dictDump', async client => {
await client.ft.dictAdd('dictionary', 'string')
assert.deepEqual(
await client.ft.dictDump('dictionary'),
['string']
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(dictionary: string): Array<string> {
return ['FT.DICTDUMP', dictionary];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,33 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from './CREATE';
import { transformArguments } from './DROPINDEX';
describe('DROPINDEX', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index'),
['FT.DROPINDEX', 'index']
);
});
it('with DD', () => {
assert.deepEqual(
transformArguments('index', { DD: true }),
['FT.DROPINDEX', 'index', 'DD']
);
});
});
testUtils.testWithClient('client.ft.dropIndex', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.TEXT // TODO: shouldn't be mandatory
});
assert.equal(
await client.ft.dropIndex('index'),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,15 @@
interface DropIndexOptions {
DD?: true;
}
export function transformArguments(index: string, options?: DropIndexOptions): Array<string> {
const args = ['FT.DROPINDEX', index];
if (options?.DD) {
args.push('DD');
}
return args;
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './EXPLAIN';
describe('EXPLAIN', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index', '*'),
['FT.EXPLAIN', 'index', '*']
);
});
});

View File

@@ -0,0 +1,7 @@
export const IS_READ_ONLY = true;
export function transformArguments(index: string, query: string): Array<string> {
return ['FT.EXPLAIN', index, query];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './EXPLAINCLI';
describe('EXPLAINCLI', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index', '*'),
['FT.EXPLAINCLI', 'index', '*']
);
});
});

View File

@@ -0,0 +1,7 @@
export const IS_READ_ONLY = true;
export function transformArguments(index: string, query: string): Array<string> {
return ['FT.EXPLAINCLI', index, query];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,65 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from './CREATE';
import { transformArguments } from './INFO';
describe('INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index'),
['FT.INFO', 'index']
);
});
testUtils.testWithClient('client.ft.info', async client => {
await client.ft.create('index', {}, {
ON: 'HASH' // TODO: shouldn't be mandatory
});
assert.deepEqual(
await client.ft.info('index'),
{
indexName: 'index',
indexOptions: [],
indexDefinition: {
defaultScore: '1',
keyType: 'HASH',
prefixes: ['']
},
attributes: [],
numDocs: '0',
maxDocId: '0',
numTerms: '0',
numRecords: '0',
invertedSzMb: '0',
totalInvertedIndexBlocks: '0',
offsetVectorsSzMb: '0',
docTableSizeMb: '0',
sortableValuesSizeMb: '0',
keyTableSizeMb: '0',
recordsPerDocAvg: '-nan',
bytesPerRecordAvg: '-nan',
offsetsPerTermAvg: '-nan',
offsetBitsPerRecordAvg: '-nan',
hashIndexingFailures: '0',
indexing: '0',
percentIndexed: '1',
gcStats: {
bytesCollected: '0',
totalMsRun: '0',
totalCycles: '0',
averageCycleTimeMs: '-nan',
lastRunTimeMs: '0',
gcNumericTreesMissed: '0',
gcBlocksDenied: '0'
},
cursorStats: {
globalIdle: 0,
globalTotal: 0,
indexCapacity: 128,
idnexTotal: 0
}
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,171 @@
export function transformArguments(index: string): Array<string> {
return ['FT.INFO', index];
}
type InfoRawReply = [
_: string,
indexName: string,
_: string,
indexOptions: Array<string>,
_: string,
indexDefinition: [
_: string,
keyType: string,
_: string,
prefixes: Array<string>,
_: string,
defaultScore: string
],
_: string,
attributes: Array<Array<string>>,
_: string,
numDocs: string,
_: string,
maxDocId: string,
_: string,
numTerms: string,
_: string,
numRecords: string,
_: string,
invertedSzMb: string,
_: string,
totalInvertedIndexBlocks: string,
_: string,
offsetVectorsSzMb: string,
_: string,
docTableSizeMb: string,
_: string,
sortableValuesSizeMb: string,
_: string,
keyTableSizeMb: string,
_: string,
recordsPerDocAvg: string,
_: string,
bytesPerRecordAvg: string,
_: string,
offsetsPerTermAvg: string,
_: string,
offsetBitsPerRecordAvg: string,
_: string,
hashIndexingFailures: string,
_: string,
indexing: string,
_: string,
percentIndexed: string,
_: string,
gcStats: [
_: string,
bytesCollected: string,
_: string,
totalMsRun: string,
_: string,
totalCycles: string,
_: string,
averageCycleTimeMs: string,
_: string,
lastRunTimeMs: string,
_: string,
gcNumericTreesMissed: string,
_: string,
gcBlocksDenied: string
],
_: string,
cursorStats: [
_: string,
globalIdle: number,
_: string,
globalTotal: number,
_: string,
indexCapacity: number,
_: string,
idnexTotal: number
]
];
interface InfoReply {
indexName: string;
indexOptions: Array<string>;
indexDefinition: {
keyType: string;
prefixes: Array<string>;
defaultScore: string;
};
attributes: Array<Array<string>>;
numDocs: string;
maxDocId: string;
numTerms: string;
numRecords: string;
invertedSzMb: string;
totalInvertedIndexBlocks: string;
offsetVectorsSzMb: string;
docTableSizeMb: string;
sortableValuesSizeMb: string;
keyTableSizeMb: string;
recordsPerDocAvg: string;
bytesPerRecordAvg: string;
offsetsPerTermAvg: string;
offsetBitsPerRecordAvg: string;
hashIndexingFailures: string;
indexing: string;
percentIndexed: string;
gcStats: {
bytesCollected: string;
totalMsRun: string;
totalCycles: string;
averageCycleTimeMs: string;
lastRunTimeMs: string;
gcNumericTreesMissed: string;
gcBlocksDenied: string;
};
cursorStats: {
globalIdle: number;
globalTotal: number;
indexCapacity: number;
idnexTotal: number;
};
}
export function transformReply(rawReply: InfoRawReply): InfoReply {
return {
indexName: rawReply[1],
indexOptions: rawReply[3],
indexDefinition: {
keyType: rawReply[5][1],
prefixes: rawReply[5][3],
defaultScore: rawReply[5][5]
},
attributes: rawReply[7],
numDocs: rawReply[9],
maxDocId: rawReply[11],
numTerms: rawReply[13],
numRecords: rawReply[15],
invertedSzMb: rawReply[17],
totalInvertedIndexBlocks: rawReply[19],
offsetVectorsSzMb: rawReply[21],
docTableSizeMb: rawReply[23],
sortableValuesSizeMb: rawReply[25],
keyTableSizeMb: rawReply[27],
recordsPerDocAvg: rawReply[29],
bytesPerRecordAvg: rawReply[31],
offsetsPerTermAvg: rawReply[33],
offsetBitsPerRecordAvg: rawReply[35],
hashIndexingFailures: rawReply[37],
indexing: rawReply[39],
percentIndexed: rawReply[41],
gcStats: {
bytesCollected: rawReply[43][1],
totalMsRun: rawReply[43][3],
totalCycles: rawReply[43][5],
averageCycleTimeMs: rawReply[43][7],
lastRunTimeMs: rawReply[43][9],
gcNumericTreesMissed: rawReply[43][11],
gcBlocksDenied: rawReply[43][13]
},
cursorStats: {
globalIdle: rawReply[45][1],
globalTotal: rawReply[45][3],
indexCapacity: rawReply[45][5],
idnexTotal: rawReply[45][7]
}
};
}

View File

@@ -0,0 +1,26 @@
export const IS_READ_ONLY = true;
interface ProfileOptions {
LIMITED?: true;
}
export function transformArguments(
index: string,
type: 'SEARCH' | 'AGGREGATE',
query: string,
options?: ProfileOptions
): Array<string> {
const args = ['FT.PROFILE', index, type];
if (options?.LIMITED) {
args.push('LIMITED');
}
args.push('QUERY', query);
return args;
}
export function transformReply() {
}

View File

@@ -0,0 +1,244 @@
import { strict as assert } from 'assert';
import { RedisSearchLanguages } from '.';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from './CREATE';
import { transformArguments } from './SEARCH';
describe('SEARCH', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 'query'),
['FT.SEARCH', 'index', 'query']
);
});
it('with VERBATIM', () => {
assert.deepEqual(
transformArguments('index', 'query', { VERBATIM: true }),
['FT.SEARCH', 'index', 'query', 'VERBATIM']
);
});
it('with NOSTOPWORDS', () => {
assert.deepEqual(
transformArguments('index', 'query', { NOSTOPWORDS: true }),
['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS']
);
});
it('with INKEYS', () => {
assert.deepEqual(
transformArguments('index', 'query', { INKEYS: 'key' }),
['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key']
);
});
it('with INFIELDS', () => {
assert.deepEqual(
transformArguments('index', 'query', { INFIELDS: 'field' }),
['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field']
);
});
it('with RETURN', () => {
assert.deepEqual(
transformArguments('index', 'query', { RETURN: 'return' }),
['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return']
);
});
describe('with SUMMARIZE', () => {
it('true', () => {
assert.deepEqual(
transformArguments('index', 'query', { SUMMARIZE: true }),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE']
);
});
describe('with FIELDS', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
FIELDS: ['@field']
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
FIELDS: ['@1', '@2']
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2']
);
});
});
it('with FRAGS', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
FRAGS: 1
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1']
);
});
it('with LEN', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
LEN: 1
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1']
);
});
it('with SEPARATOR', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
SEPARATOR: 'separator'
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator']
);
});
});
describe('with HIGHLIGHT', () => {
it('true', () => {
assert.deepEqual(
transformArguments('index', 'query', { HIGHLIGHT: true }),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT']
);
});
describe('with FIELDS', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', 'query', {
HIGHLIGHT: {
FIELDS: ['@field']
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', 'query', {
HIGHLIGHT: {
FIELDS: ['@1', '@2']
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2']
);
});
});
it('with TAGS', () => {
assert.deepEqual(
transformArguments('index', 'query', {
HIGHLIGHT: {
TAGS: {
open: 'open',
close: 'close'
}
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close']
);
});
});
it('with SLOP', () => {
assert.deepEqual(
transformArguments('index', 'query', { SLOP: 1 }),
['FT.SEARCH', 'index', 'query', 'SLOP', '1']
);
});
it('with INORDER', () => {
assert.deepEqual(
transformArguments('index', 'query', { INORDER: true }),
['FT.SEARCH', 'index', 'query', 'INORDER']
);
});
it('with LANGUAGE', () => {
assert.deepEqual(
transformArguments('index', 'query', { LANGUAGE: RedisSearchLanguages.ARABIC }),
['FT.SEARCH', 'index', 'query', 'LANGUAGE', RedisSearchLanguages.ARABIC]
);
});
it('with EXPANDER', () => {
assert.deepEqual(
transformArguments('index', 'query', { EXPANDER: 'expender' }),
['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender']
);
});
it('with SCORER', () => {
assert.deepEqual(
transformArguments('index', 'query', { SCORER: 'scorer' }),
['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer']
);
});
it('with MSORTBY', () => {
assert.deepEqual(
transformArguments('index', 'query', { MSORTBY: '@by' }),
['FT.SEARCH', 'index', 'query', 'MSORTBY', '1', '@by']
);
});
it('with LIMIT', () => {
assert.deepEqual(
transformArguments('index', 'query', {
LIMIT: {
from: 0,
size: 1
}
}),
['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1']
);
});
});
testUtils.testWithClient('client.ft.search', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
}),
client.hSet('1', 'field', '1')
]);
assert.deepEqual(
await client.ft.search('index', '*'),
{
total: 1,
documents: [{
id: '1',
value: Object.create(null, {
field: {
value: '1',
configurable: true,
enumerable: true
}
})
}]
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,200 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { pushOptionalVerdictArgument, pushVerdictArgument, transformReplyTuples } from '@node-redis/client/dist/lib/commands/generic-transformers';
import { RedisSearchLanguages, PropertyName, pushSortByArguments, SortByOptions } from '.';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
interface SearchOptions {
// NOCONTENT?: true; TODO
VERBATIM?: true;
NOSTOPWORDS?: true;
// WITHSCORES?: true;
// WITHPAYLOADS?: true;
WITHSORTKEYS?: true;
// FILTER?: {
// field: string;
// min: number | string;
// max: number | string;
// };
// GEOFILTER?: {
// field: string;
// lon: number;
// lat: number;
// radius: number;
// unit: 'm' | 'km' | 'mi' | 'ft';
// };
INKEYS?: string | Array<string>;
INFIELDS?: string | Array<string>;
RETURN?: string | Array<string>;
SUMMARIZE?: true | {
FIELDS?: PropertyName | Array<PropertyName>;
FRAGS?: number;
LEN?: number;
SEPARATOR?: string;
};
HIGHLIGHT?: true | {
FIELDS?: PropertyName | Array<PropertyName>;
TAGS?: {
open: string;
close: string;
};
};
SLOP?: number;
INORDER?: true;
LANGUAGE?: RedisSearchLanguages;
EXPANDER?: string;
SCORER?: string;
// EXPLAINSCORE?: true; // TODO: WITHSCORES
// PAYLOAD?: ;
// SORTBY?: SortByOptions;
MSORTBY?: SortByOptions | Array<SortByOptions>;
LIMIT?: {
from: number | string;
size: number | string;
};
}
export function transformArguments(
index: string,
query: string,
options?: SearchOptions
): RedisCommandArguments {
const args: RedisCommandArguments = ['FT.SEARCH', index, query];
if (options?.VERBATIM) {
args.push('VERBATIM');
}
if (options?.NOSTOPWORDS) {
args.push('NOSTOPWORDS');
}
// if (options?.WITHSCORES) {
// args.push('WITHSCORES');
// }
// if (options?.WITHPAYLOADS) {
// args.push('WITHPAYLOADS');
// }
pushOptionalVerdictArgument(args, 'INKEYS', options?.INKEYS);
pushOptionalVerdictArgument(args, 'INFIELDS', options?.INFIELDS);
pushOptionalVerdictArgument(args, 'RETURN', options?.RETURN);
if (options?.SUMMARIZE) {
args.push('SUMMARIZE');
if (typeof options.SUMMARIZE === 'object') {
if (options.SUMMARIZE.FIELDS) {
args.push('FIELDS');
pushVerdictArgument(args, options.SUMMARIZE.FIELDS);
}
if (options.SUMMARIZE.FRAGS) {
args.push('FRAGS', options.SUMMARIZE.FRAGS.toString());
}
if (options.SUMMARIZE.LEN) {
args.push('LEN', options.SUMMARIZE.LEN.toString());
}
if (options.SUMMARIZE.SEPARATOR) {
args.push('SEPARATOR', options.SUMMARIZE.SEPARATOR);
}
}
}
if (options?.HIGHLIGHT) {
args.push('HIGHLIGHT');
if (typeof options.HIGHLIGHT === 'object') {
if (options.HIGHLIGHT.FIELDS) {
args.push('FIELDS');
pushVerdictArgument(args, options.HIGHLIGHT.FIELDS);
}
if (options.HIGHLIGHT.TAGS) {
args.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close);
}
}
}
if (options?.SLOP) {
args.push('SLOP', options.SLOP.toString());
}
if (options?.INORDER) {
args.push('INORDER');
}
if (options?.LANGUAGE) {
args.push('LANGUAGE', options.LANGUAGE);
}
if (options?.EXPANDER) {
args.push('EXPANDER', options.EXPANDER);
}
if (options?.SCORER) {
args.push('SCORER', options.SCORER);
}
// if (options?.EXPLAINSCORE) {
// args.push('EXPLAINSCORE');
// }
// if (options?.PAYLOAD) {
// args.push('PAYLOAD', options.PAYLOAD);
// }
// if (options?.SORTBY) {
// args.push('SORTBY');
// pushSortByArguments(args, options.SORTBY);
// }
if (options?.MSORTBY) {
pushSortByArguments(args, 'MSORTBY', options.MSORTBY);
}
if (options?.LIMIT) {
args.push(
'LIMIT',
options.LIMIT.from.toString(),
options.LIMIT.size.toString()
);
}
return args;
}
interface SearchDocumentValue {
[key: string]: string | number | null | Array<SearchDocumentValue> | SearchDocumentValue;
}
interface SearchReply {
total: number;
documents: Array<{
id: string;
value: SearchDocumentValue;
}>;
}
export function transformReply(reply: Array<any>): SearchReply {
const documents = [];
for (let i = 1; i < reply.length; i += 2) {
const tuples = reply[i + 1];
documents.push({
id: reply[i],
value: tuples.length === 2 && tuples[0] === '$' ?
JSON.parse(tuples[1]) :
transformReplyTuples(tuples)
});
}
return {
total: reply[0],
documents
};
}

View File

@@ -0,0 +1,71 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from './CREATE';
import { transformArguments } from './SPELLCHECK';
describe('SPELLCHECK', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 'query'),
['FT.SPELLCHECK', 'index', 'query']
);
});
it('with DISTANCE', () => {
assert.deepEqual(
transformArguments('index', 'query', { DISTANCE: 2 }),
['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2']
);
});
describe('with TERMS', () => {
it('single', () => {
assert.deepEqual(
transformArguments('index', 'query', {
TERMS: {
mode: 'INCLUDE',
dictionary: 'dictionary'
}
}),
['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments('index', 'query', {
TERMS: [{
mode: 'INCLUDE',
dictionary: 'include'
}, {
mode: 'EXCLUDE',
dictionary: 'exclude'
}]
}),
['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude']
);
});
});
});
testUtils.testWithClient('client.ft.spellCheck', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.TEXT
}),
client.hSet('key', 'field', 'query')
]);
assert.deepEqual(
await client.ft.spellCheck('index', 'quer'),
[{
term: 'quer',
suggestions: [{
score: 1,
suggestion: 'query'
}]
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,57 @@
interface SpellCheckTerms {
mode: 'INCLUDE' | 'EXCLUDE';
dictionary: string;
}
interface SpellCheckOptions {
DISTANCE?: number;
TERMS?: SpellCheckTerms | Array<SpellCheckTerms>;
}
export function transformArguments(index: string, query: string, options?: SpellCheckOptions): Array<string> {
const args = ['FT.SPELLCHECK', index, query];
if (options?.DISTANCE) {
args.push('DISTANCE', options.DISTANCE.toString());
}
if (options?.TERMS) {
if (Array.isArray(options.TERMS)) {
for (const term of options.TERMS) {
pushTerms(args, term);
}
} else {
pushTerms(args, options.TERMS);
}
}
return args;
}
function pushTerms(args: Array<string>, { mode, dictionary }: SpellCheckTerms): void {
args.push('TERMS', mode, dictionary);
}
type SpellCheckRawReply = Array<[
_: string,
term: string,
suggestions: Array<[score: string, suggestion: string]>
]>;
type SpellCheckReply = Array<{
term: string,
suggestions: Array<{
score: number,
suggestion: string
}>
}>;
export function transformReply(rawReply: SpellCheckRawReply): SpellCheckReply {
return rawReply.map(([, term, suggestions]) => ({
term,
suggestions: suggestions.map(([score, suggestion]) => ({
score: Number(score),
suggestion
}))
}));
}

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGADD';
describe('SUGADD', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('key', 'string', 1),
['FT.SUGADD', 'key', 'string', '1']
);
});
it('with INCR', () => {
assert.deepEqual(
transformArguments('key', 'string', 1, { INCR: true }),
['FT.SUGADD', 'key', 'string', '1', 'INCR']
);
});
it('with PAYLOAD', () => {
assert.deepEqual(
transformArguments('key', 'string', 1, { PAYLOAD: 'payload' }),
['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload']
);
});
});
testUtils.testWithClient('client.ft.sugAdd', async client => {
assert.equal(
await client.ft.sugAdd('key', 'string', 1),
1
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,20 @@
interface SugAddOptions {
INCR?: true;
PAYLOAD?: string;
}
export function transformArguments(key: string, string: string, score: number, options?: SugAddOptions): Array<string> {
const args = ['FT.SUGADD', key, string, score.toString()];
if (options?.INCR) {
args.push('INCR');
}
if (options?.PAYLOAD) {
args.push('PAYLOAD', options.PAYLOAD);
}
return args;
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGDEL';
describe('SUGDEL', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'string'),
['FT.SUGDEL', 'key', 'string']
);
});
testUtils.testWithClient('client.ft.sugDel', async client => {
assert.equal(
await client.ft.sugDel('key', 'string'),
false
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(key: string, string: string): Array<string> {
return ['FT.SUGDEL', key, string];
}
export { transformReplyBoolean as transformReply } from '@node-redis/client/dist/lib/commands/generic-transformers';

View File

@@ -0,0 +1,46 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET';
describe('SUGGET', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix']
);
});
it('with FUZZY', () => {
assert.deepEqual(
transformArguments('key', 'prefix', { FUZZY: true }),
['FT.SUGGET', 'key', 'prefix', 'FUZZY']
);
});
it('with MAX', () => {
assert.deepEqual(
transformArguments('key', 'prefix', { MAX: 10 }),
['FT.SUGGET', 'key', 'prefix', 'MAX', '10']
);
});
});
describe('client.ft.sugGet', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGet('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1);
assert.deepEqual(
await client.ft.sugGet('key', 'string'),
['string']
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -0,0 +1,22 @@
export const IS_READ_ONLY = true;
export interface SugGetOptions {
FUZZY?: true;
MAX?: number;
}
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
const args = ['FT.SUGGET', key, prefix];
if (options?.FUZZY) {
args.push('FUZZY');
}
if (options?.MAX) {
args.push('MAX', options.MAX.toString());
}
return args;
}
export declare function transformReply(): null | Array<string>;

View File

@@ -0,0 +1,33 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET_WITHPAYLOADS';
describe('SUGGET WITHPAYLOADS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS']
);
});
describe('client.ft.sugGetWithPayloads', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithPayloads('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' });
assert.deepEqual(
await client.ft.sugGetWithPayloads('key', 'string'),
[{
suggestion: 'string',
payload: 'payload'
}]
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -0,0 +1,29 @@
import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
export { IS_READ_ONLY } from './SUGGET';
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
return [
...transformSugGetArguments(key, prefix, options),
'WITHPAYLOADS'
];
}
export interface SuggestionWithPayload {
suggestion: string;
payload: string | null;
}
export function transformReply(rawReply: Array<string | null> | null): Array<SuggestionWithPayload> | null {
if (rawReply === null) return null;
const transformedReply = [];
for (let i = 0; i < rawReply.length; i += 2) {
transformedReply.push({
suggestion: rawReply[i]!,
payload: rawReply[i + 1]
});
}
return transformedReply;
}

View File

@@ -0,0 +1,33 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET_WITHSCORES';
describe('SUGGET WITHSCORES', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHSCORES']
);
});
describe('client.ft.sugGetWithScores', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithScores('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1);
assert.deepEqual(
await client.ft.sugGetWithScores('key', 'string'),
[{
suggestion: 'string',
score: 2147483648
}]
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -0,0 +1,29 @@
import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
export { IS_READ_ONLY } from './SUGGET';
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
return [
...transformSugGetArguments(key, prefix, options),
'WITHSCORES'
];
}
export interface SuggestionWithScores {
suggestion: string;
score: number;
}
export function transformReply(rawReply: Array<string> | null): Array<SuggestionWithScores> | null {
if (rawReply === null) return null;
const transformedReply = [];
for (let i = 0; i < rawReply.length; i += 2) {
transformedReply.push({
suggestion: rawReply[i],
score: Number(rawReply[i + 1])
});
}
return transformedReply;
}

View File

@@ -0,0 +1,34 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET_WITHSCORES_WITHPAYLOADS';
describe('SUGGET WITHSCORES WITHPAYLOADS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS']
);
});
describe('client.ft.sugGetWithScoresWithPayloads', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' });
assert.deepEqual(
await client.ft.sugGetWithScoresWithPayloads('key', 'string'),
[{
suggestion: 'string',
score: 2147483648,
payload: 'payload'
}]
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -0,0 +1,30 @@
import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
import { SuggestionWithPayload } from './SUGGET_WITHPAYLOADS';
import { SuggestionWithScores } from './SUGGET_WITHSCORES';
export { IS_READ_ONLY } from './SUGGET';
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
return [
...transformSugGetArguments(key, prefix, options),
'WITHSCORES',
'WITHPAYLOADS'
];
}
type SuggestionWithScoresAndPayloads = SuggestionWithScores & SuggestionWithPayload;
export function transformReply(rawReply: Array<string | null> | null): Array<SuggestionWithScoresAndPayloads> | null {
if (rawReply === null) return null;
const transformedReply = [];
for (let i = 0; i < rawReply.length; i += 3) {
transformedReply.push({
suggestion: rawReply[i]!,
score: Number(rawReply[i + 1]!),
payload: rawReply[i + 2]
});
}
return transformedReply;
}

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGLEN';
describe('SUGLEN', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['FT.SUGLEN', 'key']
);
});
testUtils.testWithClient('client.ft.sugLen', async client => {
assert.equal(
await client.ft.sugLen('key'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,7 @@
export const IS_READ_ONLY = true;
export function transformArguments(key: string): Array<string> {
return ['FT.SUGLEN', key];
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SYNDUMP';
describe('SYNDUMP', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index'),
['FT.SYNDUMP', 'index']
);
});
testUtils.testWithClient('client.ft.synDump', async client => {
await client.ft.create('index', {}, {
ON: 'HASH' // TODO: shouldn't be mandatory
});
assert.deepEqual(
await client.ft.synDump('index'),
[]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(index: string): Array<string> {
return ['FT.SYNDUMP', index];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,39 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SYNUPDATE';
describe('SYNUPDATE', () => {
describe('transformArguments', () => {
it('single term', () => {
assert.deepEqual(
transformArguments('index', 'groupId', 'term'),
['FT.SYNUPDATE', 'index', 'groupId', 'term']
);
});
it('multiple terms', () => {
assert.deepEqual(
transformArguments('index', 'groupId', ['1', '2']),
['FT.SYNUPDATE', 'index', 'groupId', '1', '2']
);
});
it('with SKIPINITIALSCAN', () => {
assert.deepEqual(
transformArguments('index', 'groupId', 'term', { SKIPINITIALSCAN: true }),
['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term']
);
});
});
testUtils.testWithClient('client.ft.synUpdate', async client => {
await client.ft.create('index', {}, {
ON: 'HASH' // TODO: shouldn't be mandatory
});
assert.equal(
await client.ft.synUpdate('index', 'groupId', 'term'),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,23 @@
import { pushVerdictArguments } from '@node-redis/client/dist/lib/commands/generic-transformers';
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
interface SynUpdateOptions {
SKIPINITIALSCAN?: true;
}
export function transformArguments(
index: string,
groupId: string,
terms: string | Array<string>,
options?: SynUpdateOptions
): RedisCommandArguments {
const args = ['FT.SYNUPDATE', index, groupId];
if (options?.SKIPINITIALSCAN) {
args.push('SKIPINITIALSCAN');
}
return pushVerdictArguments(args, terms);
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,24 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from './CREATE';
import { transformArguments } from './TAGVALS';
describe('TAGVALS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index', '@field'),
['FT.TAGVALS', 'index', '@field']
);
});
testUtils.testWithClient('client.ft.tagVals', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.TAG
});
assert.deepEqual(
await client.ft.tagVals('index', 'field'),
[]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(index: string, fieldName: string): Array<string> {
return ['FT.TAGVALS', index, fieldName];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './_LIST';
describe('_LIST', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['FT._LIST']
);
});
testUtils.testWithClient('client.ft._list', async client => {
assert.deepEqual(
await client.ft._list(),
[]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['FT._LIST'];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,46 @@
import { strict as assert } from 'assert';
import { pushArgumentsWithLength, pushSortByArguments } from '.';
describe('pushSortByArguments', () => {
describe('single', () => {
it('string', () => {
assert.deepEqual(
pushSortByArguments([], 'SORTBT', '@property'),
['SORTBT', '1', '@property']
);
});
it('.BY', () => {
assert.deepEqual(
pushSortByArguments([], 'SORTBT', { BY: '@property' }),
['SORTBT', '1', '@property']
);
});
it('with DIRECTION', () => {
assert.deepEqual(
pushSortByArguments([], 'SORTBY', {
BY: '@property',
DIRECTION: 'ASC'
}),
['SORTBY', '2', '@property', 'ASC']
);
});
});
it('multiple', () => {
assert.deepEqual(
pushSortByArguments([], 'SORTBY', ['@1', '@2']),
['SORTBY', '2', '@1', '@2']
);
});
});
it('pushArgumentsWithLength', () => {
assert.deepEqual(
pushArgumentsWithLength(['a'], args => {
args.push('b', 'c');
}),
['a', '2', 'b', 'c']
);
});

View File

@@ -0,0 +1,161 @@
import * as _LIST from './_LIST';
import * as AGGREGATE from './AGGREGATE';
import * as ALIASADD from './ALIASADD';
import * as ALIASDEL from './ALIASDEL';
import * as ALIASUPDATE from './ALIASUPDATE';
import * as CONFIG_GET from './CONFIG_GET';
import * as CONFIG_SET from './CONFIG_SET';
import * as CREATE from './CREATE';
import * as DICTADD from './DICTADD';
import * as DICTDEL from './DICTDEL';
import * as DICTDUMP from './DICTDUMP';
import * as DROPINDEX from './DROPINDEX';
import * as EXPLAIN from './EXPLAIN';
import * as EXPLAINCLI from './EXPLAINCLI';
import * as INFO from './INFO';
// import * as PROFILE from './PROFILE';
import * as SEARCH from './SEARCH';
import * as SPELLCHECK from './SPELLCHECK';
import * as SUGADD from './SUGADD';
import * as SUGDEL from './SUGDEL';
import * as SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS';
import * as SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS';
import * as SUGGET_WITHSCORES from './SUGGET_WITHSCORES';
import * as SUGGET from './SUGGET';
import * as SUGLEN from './SUGLEN';
import * as SYNDUMP from './SYNDUMP';
import * as SYNUPDATE from './SYNUPDATE';
import * as TAGVALS from './TAGVALS';
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
export default {
_LIST,
_list: _LIST,
AGGREGATE,
aggregate: AGGREGATE,
ALIASADD,
aliasAdd: ALIASADD,
ALIASDEL,
aliasDel: ALIASDEL,
ALIASUPDATE,
aliasUpdate: ALIASUPDATE,
CONFIG_GET,
configGet: CONFIG_GET,
CONFIG_SET,
configSet: CONFIG_SET,
CREATE,
create: CREATE,
DICTADD,
dictAdd: DICTADD,
DICTDEL,
dictDel: DICTDEL,
DICTDUMP,
dictDump: DICTDUMP,
DROPINDEX,
dropIndex: DROPINDEX,
EXPLAIN,
explain: EXPLAIN,
EXPLAINCLI,
explainCli: EXPLAINCLI,
INFO,
info: INFO,
// PROFILE,
// profile: PROFILE,
SEARCH,
search: SEARCH,
SPELLCHECK,
spellCheck: SPELLCHECK,
SUGADD,
sugAdd: SUGADD,
SUGDEL,
sugDel: SUGDEL,
SUGGET_WITHPAYLOADS,
sugGetWithPayloads: SUGGET_WITHPAYLOADS,
SUGGET_WITHSCORES_WITHPAYLOADS,
sugGetWithScoresWithPayloads: SUGGET_WITHSCORES_WITHPAYLOADS,
SUGGET_WITHSCORES,
sugGetWithScores: SUGGET_WITHSCORES,
SUGGET,
sugGet: SUGGET,
SUGLEN,
sugLen: SUGLEN,
SYNDUMP,
synDump: SYNDUMP,
SYNUPDATE,
synUpdate: SYNUPDATE,
TAGVALS,
tagVals: TAGVALS
};
export enum RedisSearchLanguages {
ARABIC = 'Arabic',
BASQUE = 'Basque',
CATALANA = 'Catalan',
DANISH = 'Danish',
DUTCH = 'Dutch',
ENGLISH = 'English',
FINNISH = 'Finnish',
FRENCH = 'French',
GERMAN = 'German',
GREEK = 'Greek',
HUNGARIAN = 'Hungarian',
INDONESAIN = 'Indonesian',
IRISH = 'Irish',
ITALIAN = 'Italian',
LITHUANIAN = 'Lithuanian',
NEPALI = 'Nepali',
NORWEIGAN = 'Norwegian',
PORTUGUESE = 'Portuguese',
ROMANIAN = 'Romanian',
RUSSIAN = 'Russian',
SPANISH = 'Spanish',
SWEDISH = 'Swedish',
TAMIL = 'Tamil',
TURKISH = 'Turkish',
CHINESE = 'Chinese'
}
export type PropertyName = `${'@' | '$.'}${string}`;
export type SortByOptions = PropertyName | {
BY: PropertyName;
DIRECTION?: 'ASC' | 'DESC';
};
function pushSortByProperty(args: RedisCommandArguments, sortBy: SortByOptions): void {
if (typeof sortBy === 'string') {
args.push(sortBy);
} else {
args.push(sortBy.BY);
if (sortBy.DIRECTION) {
args.push(sortBy.DIRECTION);
}
}
}
export function pushSortByArguments(args: RedisCommandArguments, name: string, sortBy: SortByOptions | Array<SortByOptions>): RedisCommandArguments {
const lengthBefore = args.push(
name,
'' // will be overwritten
);
if (Array.isArray(sortBy)) {
for (const field of sortBy) {
pushSortByProperty(args, field);
}
} else {
pushSortByProperty(args, sortBy);
}
args[lengthBefore - 1] = (args.length - lengthBefore).toString();
return args;
}
export function pushArgumentsWithLength(args: RedisCommandArguments, fn: (args: RedisCommandArguments) => void): RedisCommandArguments {
const lengthIndex = args.push('') - 1;
fn(args);
args[lengthIndex] = (args.length - lengthIndex - 1).toString();
return args;
}

View File

@@ -0,0 +1,4 @@
export { default } from './commands';
export { SchemaFieldTypes, SchemaTextFieldPhonetics } from './commands/CREATE';
export { AggregateSteps, AggregateGroupByReducers } from './commands/AGGREGATE';

View File

@@ -0,0 +1,21 @@
import TestUtils from '@node-redis/test-utils';
import RediSearch from '.';
export default new TestUtils({
dockerImageName: 'redislabs/redisearch',
dockerImageVersionArgument: 'redisearch-version',
defaultDockerVersion: '2.2.1'
});
export const GLOBAL = {
SERVERS: {
OPEN: {
serverArguments: ['--loadmodule /usr/lib/redis/modules/redisearch.so'],
clientOptions: {
modules: {
ft: RediSearch
}
}
}
}
};

View File

@@ -0,0 +1,24 @@
{
"name": "@node-redis/search",
"version": "1.0.0-rc.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
"build": "tsc"
},
"peerDependencies": {
"@node-redis/client": "^1.0.0-rc"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@node-redis/test-utils": "*",
"@types/node": "^16.11.7",
"nyc": "^15.1.0",
"release-it": "^14.11.7",
"source-map-support": "^0.5.20",
"ts-node": "^10.4.0",
"typescript": "^4.4.4"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"./lib/**/*.ts"
]
}