From 4b4eea80c53e7b021a9349b7b92e069c3693736f Mon Sep 17 00:00:00 2001 From: sky3d Date: Thu, 15 Dec 2022 16:49:21 +0300 Subject: [PATCH 1/2] fix: add aggregation search method --- src/actions/list.js | 77 +-- src/configs/core.js | 1 + src/configs/redis-indexes.js | 2 +- .../redis-search-stack/build-filter-query.js | 22 + .../redis-search-stack/create-hash-index.js | 4 +- src/utils/redis-search-stack/expressions.js | 1 + .../redis-search-stack/extract-user-id.js | 3 + src/utils/redis-search-stack/index.js | 9 +- .../redis-search-stack/query-aggregate.js | 60 ++ src/utils/redis-search-stack/query-search.js | 55 ++ test/suites/actions/list-search.js | 516 +++++++++--------- test/suites/actions/list.js | 1 + 12 files changed, 427 insertions(+), 324 deletions(-) create mode 100644 src/utils/redis-search-stack/build-filter-query.js create mode 100644 src/utils/redis-search-stack/extract-user-id.js create mode 100644 src/utils/redis-search-stack/query-aggregate.js create mode 100644 src/utils/redis-search-stack/query-search.js diff --git a/src/actions/list.js b/src/actions/list.js index 3c598cb93..7d83fa0a9 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -14,15 +14,13 @@ const { } = require('../constants'); const { - buildSearchQuery, - normalizeFilterProp, + redisSearchQuery, + redisAggregateQuery, } = require('../utils/redis-search-stack'); // helper const JSONParse = (data) => JSON.parse(data); -const extractUserId = (keyPrefix) => (userKey) => userKey.split('!')[0].slice(keyPrefix.length); - // fetches basic ids async function fetchIds() { const { @@ -51,66 +49,21 @@ async function fetchIds() { async function redisSearchIds() { const { service, - redis, - args: request, + args, filter = {}, audience, - offset, - limit, + redisSearchConfig, } = this; - service.log.debug({ criteria: request.criteria, filter }, 'users list searching...'); - const { keyPrefix } = service.config.redis.options; - - const { indexName, multiWords } = service.redisSearch.getIndexMetadata(audience); - - service.log.debug('search using index: %s', indexName); - const args = ['FT.SEARCH', indexName]; - - const query = []; - const params = []; - - for (const [propName, actionTypeOrValue] of Object.entries(filter)) { - const prop = normalizeFilterProp(propName, actionTypeOrValue); - - if (actionTypeOrValue !== undefined) { - const [sQuery, sParams] = buildSearchQuery(prop, actionTypeOrValue, { multiWords }); - - query.push(sQuery); - params.push(...sParams); // name, value - } - } - - if (query.length > 0) { - args.push(query.join(' ')); - } else { - args.push('*'); - } - - // TODO extract to redis aearch utils - if (params.length > 0) { - args.push('PARAMS', params.length, ...params); - args.push('DIALECT', '2'); // use params dialect - } - - // sort the response - if (request.criteria) { - args.push('SORTBY', request.criteria, request.order); - } - // limits - args.push('LIMIT', offset, limit); - - // we'll fetch the data later - args.push('NOCONTENT'); - - // [total, [ids]] - service.log.info('redis search query: %s', args.join(' ')); + service.log.debug({ criteria: args.criteria, filter }, 'users list searching...'); - const [total, ...keys] = await redis.call(...args); + const useAggregation = redisSearchConfig.queryMethod === 'aggregate'; + const searchQuery = useAggregation ? redisAggregateQuery : redisSearchQuery; - const extractId = extractUserId(keyPrefix); + const indexMeta = service.redisSearch.getIndexMetadata(audience); + service.log.debug('search using index: %s', indexMeta.indexName); - const ids = keys.map(extractId); + const { total, ids } = await searchQuery(indexMeta, this); service.log.info({ ids }, 'search result: %d', total); @@ -135,7 +88,7 @@ async function fetchUserData(ids) { service, redis, audience, - seachEnabled, + redisSearchConfig, offset, limit, userIdsOnly, @@ -143,7 +96,7 @@ async function fetchUserData(ids) { let dataKey = USERS_METADATA; - if (seachEnabled) { + if (redisSearchConfig.enabled) { const meta = service.redisSearch.getIndexMetadata(audience); dataKey = meta.filterKey; } @@ -219,14 +172,14 @@ module.exports = function iterateOverActiveUsers({ params }) { } else if (params.public === undefined || params.public === false) { index = USERS_INDEX; } else { - index = `${USERS_REFERRAL_INDEX}:${params.public}`; + index = `${USERS_REFERRAL_INDEX}: ${params.public}`; } const ctx = { // service parts redis, - seachEnabled: config.redisSearch.enabled, service: this, + redisSearchConfig: config.redisSearch, // input parts for lua script keys: [ @@ -251,7 +204,7 @@ module.exports = function iterateOverActiveUsers({ params }) { audience, }; - const findUserIds = ctx.seachEnabled ? redisSearchIds : fetchIds; + const findUserIds = ctx.redisSearchConfig.enabled ? redisSearchIds : fetchIds; return Promise .bind(ctx) diff --git a/src/configs/core.js b/src/configs/core.js index 85b57684e..283886bbe 100644 --- a/src/configs/core.js +++ b/src/configs/core.js @@ -136,4 +136,5 @@ exports.mfa = { */ exports.redisSearch = { enabled: false, + queryMethod: 'search', // search | aggregate }; diff --git a/src/configs/redis-indexes.js b/src/configs/redis-indexes.js index dbf299ab3..c0a139dde 100644 --- a/src/configs/redis-indexes.js +++ b/src/configs/redis-indexes.js @@ -4,7 +4,7 @@ */ exports.redisIndexDefinitions = [ - // Index Name: {ms-users}-metadata-*.localhost-idx + // Index Name: {ms-users}-metadata-*.localhost-v1-idx // Index Filter: metadata!*.localhost { version: '1', diff --git a/src/utils/redis-search-stack/build-filter-query.js b/src/utils/redis-search-stack/build-filter-query.js new file mode 100644 index 000000000..baad955ef --- /dev/null +++ b/src/utils/redis-search-stack/build-filter-query.js @@ -0,0 +1,22 @@ +const buildSearchQuery = require('./build-search-query'); +const normalizeFilterProp = require('./normalize-filter-prop'); + +function buildFilterQuery(filter, multiWords) { + const query = []; + const params = []; + + for (const [propName, actionTypeOrValue] of Object.entries(filter)) { + const prop = normalizeFilterProp(propName, actionTypeOrValue); + + if (actionTypeOrValue !== undefined) { + const [sQuery, sParams] = buildSearchQuery(prop, actionTypeOrValue, { multiWords }); + + query.push(sQuery); + params.push(...sParams); // name, value + } + } + + return [query, params]; +} + +module.exports = buildFilterQuery; diff --git a/src/utils/redis-search-stack/create-hash-index.js b/src/utils/redis-search-stack/create-hash-index.js index 0127da12d..e139aa191 100644 --- a/src/utils/redis-search-stack/create-hash-index.js +++ b/src/utils/redis-search-stack/create-hash-index.js @@ -1,4 +1,6 @@ const redisKey = require('../key'); +const { containsKeyExpr } = require('./expressions'); + /** * @param {Object} service provides redis, log * @param {Object} keyPrefix root prefix, e.g. {ms-users} @@ -14,7 +16,7 @@ async function createHashIndex({ redis, log }, indexName, prefix, filter, fields if (filter) { const key = redisKey('', filter); // leading separator filterExpr.push('FILTER'); - filterExpr.push(`'contains(@__key, ${key}) > 0'`); + filterExpr.push(containsKeyExpr(key)); } try { diff --git a/src/utils/redis-search-stack/expressions.js b/src/utils/redis-search-stack/expressions.js index 83bd5c542..77047f07a 100644 --- a/src/utils/redis-search-stack/expressions.js +++ b/src/utils/redis-search-stack/expressions.js @@ -34,4 +34,5 @@ module.exports = exports = { // Utils tokenize: (value) => value.replace(PUNCTUATION_REGEX, ' ').split(/\s/), + containsKeyExpr: (value) => `contains(@__key, "${value}")`, }; diff --git a/src/utils/redis-search-stack/extract-user-id.js b/src/utils/redis-search-stack/extract-user-id.js new file mode 100644 index 000000000..f1da1bd12 --- /dev/null +++ b/src/utils/redis-search-stack/extract-user-id.js @@ -0,0 +1,3 @@ +const extractUserId = (keyPrefix) => (userKey) => userKey.split('!')[0].slice(keyPrefix.length); + +module.exports = extractUserId; diff --git a/src/utils/redis-search-stack/index.js b/src/utils/redis-search-stack/index.js index 65f222775..bf4aa874b 100644 --- a/src/utils/redis-search-stack/index.js +++ b/src/utils/redis-search-stack/index.js @@ -1,11 +1,12 @@ const ensureSearchIndexes = require('./ensure-indexes'); -const normalizeFilterProp = require('./normalize-filter-prop'); const normalizeIndexName = require('./normalize-index-name'); -const buildSearchQuery = require('./build-search-query'); + +const redisSearchQuery = require('./query-search'); +const redisAggregateQuery = require('./query-aggregate'); module.exports = { ensureSearchIndexes, - normalizeFilterProp, normalizeIndexName, - buildSearchQuery, + redisSearchQuery, + redisAggregateQuery, }; diff --git a/src/utils/redis-search-stack/query-aggregate.js b/src/utils/redis-search-stack/query-aggregate.js new file mode 100644 index 000000000..9817b923f --- /dev/null +++ b/src/utils/redis-search-stack/query-aggregate.js @@ -0,0 +1,60 @@ +const redisKey = require('../key'); +const extractUserId = require('./extract-user-id'); +const { containsKeyExpr } = require('./expressions'); +const buildFilterQuery = require('./build-filter-query'); + +async function redisAggregateQuery(indexMeta, context) { + const { + service, + redis, + args: request, + filter = {}, + audience, + offset, + limit, + } = context; + + const { keyPrefix } = service.config.redis.options; + + const { indexName, filterKey, multiWords } = indexMeta; + + const args = ['FT.AGGREGATE', indexName]; + + const [query, params] = buildFilterQuery(filter, multiWords); + + if (query.length > 0) { + args.push(query.join(' ')); + } else { + args.push('*'); + } + + const load = ['@id', '@__key']; // TODO field from config + args.push('LOAD', load.length, ...load); + + const filterCondition = redisKey('', filterKey, audience); // with leading separator + args.push('FILTER', containsKeyExpr(filterCondition)); + + // TODO extract to redis aearch utils + if (params.length > 0) { + args.push('PARAMS', params.length, ...params); + args.push('DIALECT', '2'); // use params dialect + } + + // sort the response + if (request.criteria) { + args.push('SORTBY', request.criteria, request.order); + } + // limits + args.push('LIMIT', offset, limit); + + service.log.info('redis aggregate query: %s', args.join(' ')); + + const [total, ...keys] = await redis.call(...args); + + const extractId = extractUserId(keyPrefix); + const ids = keys.map(([, key]) => extractId(key)); + + return { total, ids }; +} + +module.exports = redisAggregateQuery; diff --git a/src/utils/redis-search-stack/query-search.js b/src/utils/redis-search-stack/query-search.js new file mode 100644 index 000000000..114c286b4 --- /dev/null +++ b/src/utils/redis-search-stack/query-search.js @@ -0,0 +1,55 @@ +const buildFilterQuery = require('./build-filter-query'); +const extractUserId = require('./extract-user-id'); + +async function redisSearchQuery(indexMeta, context) { + const { + service, + redis, + args: request, + filter = {}, + offset, + limit, + } = context; + + const { keyPrefix } = service.config.redis.options; + const { indexName, multiWords } = indexMeta; + + const args = ['FT.SEARCH', indexName]; + + const [query, params] = buildFilterQuery(filter, multiWords); + + if (query.length > 0) { + args.push(query.join(' ')); + } else { + args.push('*'); + } + + // TODO extract to redis aearch utils + if (params.length > 0) { + args.push('PARAMS', params.length, ...params); + args.push('DIALECT', '2'); // use params dialect + } + + // sort the response + if (request.criteria) { + args.push('SORTBY', request.criteria, request.order); + } + // limits + args.push('LIMIT', offset, limit); + + // we'll fetch the data later + args.push('NOCONTENT'); + + // [total, [ids]] + service.log.info('redis search query: %s', args.join(' ')); + + const [total, ...keys] = await redis.call(...args); + + const extractId = extractUserId(keyPrefix); + + const ids = keys.map(extractId); + + return { total, ids }; +} + +module.exports = redisSearchQuery; diff --git a/test/suites/actions/list-search.js b/test/suites/actions/list-search.js index 3b1b88dd5..aa2f44053 100644 --- a/test/suites/actions/list-search.js +++ b/test/suites/actions/list-search.js @@ -52,300 +52,304 @@ function listRequest(filter, criteria = 'username') { }); } -describe('Redis Search: list', function listSuite() { - this.timeout(50000); - - const ctx = { - redisSearch: { - enabled: true, - }, - redisIndexDefinitions, - }; - - const totalUsers = 10; - - beforeEach(async function startService() { - await global.startService.call(this, ctx); - }); - afterEach('reset redis', global.clearRedis); +for (const queryMethod of ['search', 'aggregate']) { // testing in two mode + describe(`Redis Search [FT:${queryMethod}]: list`, function listSuite() { + this.timeout(50000); + + const ctx = { + redisSearch: { + enabled: true, + queryMethod, + }, + redisIndexDefinitions, + }; - beforeEach('populate redis', function populateRedis() { - const audience = this.users.config.jwt.defaultAudience; - const promises = []; + const totalUsers = 10; - ld.times(totalUsers, () => { - const user = createUser(this.users.flake.next()); - const item = saveUser(this.users.redis, USERS_METADATA, audience, user); - promises.push(item); + beforeEach(async function startService() { + await global.startService.call(this, ctx); }); + afterEach('reset redis', global.clearRedis); - const people = [ - { username: 'ann@gmail.org', firstName: 'Ann', lastName: faker.lastName }, - { username: 'johnny@gmail.org', firstName: 'Johhny', lastName: faker.lastName }, - { username: 'joe@yahoo.org', firstName: 'Joe', lastName: null }, - { username: 'ann@yahoo.org', firstName: 'Anna', lastName: faker.lastName }, - { username: 'kim@yahoo.org', firstName: 'Kim', lastName: 'Johhny' }, - ]; + beforeEach('populate redis', function populateRedis() { + const audience = this.users.config.jwt.defaultAudience; + const promises = []; - for (let i = 0; i < people.length; i += 1) { - const item = people[i]; - const userId = this.users.flake.next(); - const user = createUser(userId, { ...item }); + ld.times(totalUsers, () => { + const user = createUser(this.users.flake.next()); + const item = saveUser(this.users.redis, USERS_METADATA, audience, user); + promises.push(item); + }); - const inserted = saveUser(this.users.redis, USERS_METADATA, audience, user); - promises.push(inserted); + const people = [ + { username: 'ann@gmail.org', firstName: 'Ann', lastName: faker.lastName }, + { username: 'johnny@gmail.org', firstName: 'Johhny', lastName: faker.lastName }, + { username: 'joe@yahoo.org', firstName: 'Joe', lastName: null }, + { username: 'ann@yahoo.org', firstName: 'Anna', lastName: faker.lastName }, + { username: 'kim@yahoo.org', firstName: 'Kim', lastName: 'Johhny' }, + ]; - const { username } = item; + for (let i = 0; i < people.length; i += 1) { + const item = people[i]; + const userId = this.users.flake.next(); + const user = createUser(userId, { ...item }); - const api = createUserApi(userId, { email: username, level: (i + 1) * 10 }); - const data = saveUser(this.users.redis, TEST_CATEGORY, TEST_AUDIENCE, api); - promises.push(data); - } + const inserted = saveUser(this.users.redis, USERS_METADATA, audience, user); + promises.push(inserted); - this.audience = audience; - this.extractUserName = getUserName(this.audience); + const { username } = item; - this.filteredListRequest = listRequest.bind(this); + const api = createUserApi(userId, { email: username, level: (i + 1) * 10 }); + const data = saveUser(this.users.redis, TEST_CATEGORY, TEST_AUDIENCE, api); + promises.push(data); + } - this.userStubs = Promise.all(promises); - return this.userStubs; - }); + this.audience = audience; + this.extractUserName = getUserName(this.audience); - it('responds with error when index not created', async function test() { - const query = { - params: { - audience: 'not-existing-audience', - }, - }; + this.filteredListRequest = listRequest.bind(this); - await assert.rejects( - this.users.dispatch('list', query), - /Search index does not registered for/ - ); - }); + this.userStubs = Promise.all(promises); + return this.userStubs; + }); - it('list by username', function test() { - return this - .users - .dispatch('list', { + it('responds with error when index not created', async function test() { + const query = { params: { - offset: 0, - limit: 10, - criteria: 'username', // sort by - audience: this.audience, - filter: { - username: 'yahoo', - }, + audience: 'not-existing-audience', }, - }) - .then((result) => { - expect(result.users).to.have.length.lte(10); - expect(result.users.length); - - result.users.forEach((user) => { - expect(user).to.have.ownProperty('id'); - expect(user).to.have.ownProperty('metadata'); - expect(user.metadata[this.audience]).to.have.ownProperty('firstName'); - expect(user.metadata[this.audience]).to.have.ownProperty('lastName'); - }); + }; - const copy = [].concat(result.users); - sortByCaseInsensitive(this.extractUserName)(copy); + await assert.rejects( + this.users.dispatch('list', query), + /Search index does not registered for/ + ); + }); - copy.forEach((data) => { - expect(data.metadata[this.audience].username).to.match(/yahoo/i); + it('list by username', function test() { + return this + .users + .dispatch('list', { + params: { + offset: 0, + limit: 10, + criteria: 'username', // sort by + audience: this.audience, + filter: { + username: 'yahoo', + }, + }, + }) + .then((result) => { + expect(result.users).to.have.length.lte(10); + expect(result.users.length); + console.log('USERS==', result.users); + + result.users.forEach((user) => { + expect(user).to.have.ownProperty('id'); + expect(user).to.have.ownProperty('metadata'); + expect(user.metadata[this.audience]).to.have.ownProperty('firstName'); + expect(user.metadata[this.audience]).to.have.ownProperty('lastName'); + }); + + const copy = [].concat(result.users); + sortByCaseInsensitive(this.extractUserName)(copy); + + copy.forEach((data) => { + expect(data.metadata[this.audience].username).to.match(/yahoo/i); + }); + + expect(copy).to.be.deep.eq(result.users); }); + }); - expect(copy).to.be.deep.eq(result.users); - }); - }); - - it('list by first name', function test() { - return this - .filteredListRequest({ firstName: 'Johhny' }, 'firstName') - .then((result) => { - assert(result); - expect(result.users).to.have.length(1); - const [u1] = result.users; - - assert(u1); - const uname = this.extractUserName(u1); - expect(uname).to.be.equal('johnny@gmail.org'); - }); - }); + it('list by first name', function test() { + return this + .filteredListRequest({ firstName: 'Johhny' }, 'firstName') + .then((result) => { + assert(result); + expect(result.users).to.have.length(1); + const [u1] = result.users; + + assert(u1); + const uname = this.extractUserName(u1); + expect(uname).to.be.equal('johnny@gmail.org'); + }); + }); - it('list by multi-word email', function test() { - return this - .filteredListRequest({ username: 'ann@gmail.org' }) - .then((result) => { - assert(result); - assert(result.users.length); - expect(result.users).to.have.length(1); + it('list by multi-word email', function test() { + return this + .filteredListRequest({ username: 'ann@gmail.org' }) + .then((result) => { + assert(result); + assert(result.users.length); + expect(result.users).to.have.length(1); - expect(this.extractUserName(result.users[0])).to.be.equal('ann@gmail.org'); - }); - // @username:$f_username_1 @username:$f_username_2 @username:$f_username_3 - // PARAMS 6 f_username_1 ann f_username_2 gmail f_username_3 - }); + expect(this.extractUserName(result.users[0])).to.be.equal('ann@gmail.org'); + }); + // @username:$f_username_1 @username:$f_username_2 @username:$f_username_3 + // PARAMS 6 f_username_1 ann f_username_2 gmail f_username_3 + }); - it('list using partial username', function test() { - return this - .filteredListRequest({ username: 'yahoo.org' }) - .then((result) => { - assert(result); - assert(result.users.length); - }); - }); + it('list using partial username', function test() { + return this + .filteredListRequest({ username: 'yahoo.org' }) + .then((result) => { + assert(result); + assert(result.users.length); + }); + }); - it('user list if username has only 1 token', function test() { - return this - .filteredListRequest({ username: 'org' }) - .then((result) => { - assert(result); - expect(result.users).to.have.length.gte(4); - }); - }); + it('user list if username has only 1 token', function test() { + return this + .filteredListRequest({ username: 'org' }) + .then((result) => { + assert(result); + expect(result.users).to.have.length.gte(4); + }); + }); - it('list with #multi fields', function test() { - return this - .filteredListRequest({ - '#multi': { - fields: [ - 'firstName', - 'lastName', - ], - match: 'Johhny', - }, - }) - .then((result) => { - assert(result); - expect(result.users).to.have.length.gte(2); + it('list with #multi fields', function test() { + return this + .filteredListRequest({ + '#multi': { + fields: [ + 'firstName', + 'lastName', + ], + match: 'Johhny', + }, + }) + .then((result) => { + assert(result); + expect(result.users).to.have.length.gte(2); - const copy = [].concat(result.users); - sortByCaseInsensitive(this.extractUserName)(copy); + const copy = [].concat(result.users); + sortByCaseInsensitive(this.extractUserName)(copy); - const [u1, u2] = copy; - expect(this.extractUserName(u1)).to.be.equal('johnny@gmail.org'); - expect(this.extractUserName(u2)).to.be.equal('kim@yahoo.org'); - }); - }); + const [u1, u2] = copy; + expect(this.extractUserName(u1)).to.be.equal('johnny@gmail.org'); + expect(this.extractUserName(u2)).to.be.equal('kim@yahoo.org'); + }); + }); - it('list: EQ action', function test() { - return this - .filteredListRequest({ username: { eq: 'kim@yahoo.org' } }) - .then((result) => { - assert(result); - expect(result.users).to.have.length(0); - }); - }); + it('list: EQ action', function test() { + return this + .filteredListRequest({ username: { eq: 'kim@yahoo.org' } }) + .then((result) => { + assert(result); + expect(result.users).to.have.length(0); + }); + }); - it('list: MATCH action with one token', function test() { - // @firstName:($f_firstName_m*) PARAMS 2 f_firstName_m Johhny - return this - .filteredListRequest({ firstName: { match: 'Johhny' } }) - .then((result) => { - assert(result); - expect(result.users).to.have.length.gte(1); - }); - }); + it('list: MATCH action with one token', function test() { + // @firstName:($f_firstName_m*) PARAMS 2 f_firstName_m Johhny + return this + .filteredListRequest({ firstName: { match: 'Johhny' } }) + .then((result) => { + assert(result); + expect(result.users).to.have.length.gte(1); + }); + }); - it('list: MATCH action with many tokens', function test() { - // @username:($f_username_m*) PARAMS 2 f_username_m \"johnny@gmail.org\" - return this - .filteredListRequest({ username: { match: 'johnny@gmail.org"' } }) - .then((result) => { - assert(result); - expect(result.users).to.have.length(0); - }); - }); + it('list: MATCH action with many tokens', function test() { + // @username:($f_username_m*) PARAMS 2 f_username_m \"johnny@gmail.org\" + return this + .filteredListRequest({ username: { match: 'johnny@gmail.org"' } }) + .then((result) => { + assert(result); + expect(result.users).to.have.length(0); + }); + }); - it('list: NE action', function test() { - return this - .filteredListRequest({ username: { ne: 'gmail' } }) - .then((result) => { - assert(result); - expect(result.users).to.have.length.gte(2); - - result.users.forEach((user) => { - const username = this.extractUserName(user); - const domain = username.split('@')[1]; - expect(domain).to.have.length.gte(1); - // TODO expect(domain.includes('gmail')).to.equal(false) + it('list: NE action', function test() { + return this + .filteredListRequest({ username: { ne: 'gmail' } }) + .then((result) => { + assert(result); + expect(result.users).to.have.length.gte(2); + + result.users.forEach((user) => { + const username = this.extractUserName(user); + const domain = username.split('@')[1]; + expect(domain).to.have.length.gte(1); + // TODO expect(domain.includes('gmail')).to.equal(false) + }); }); - }); - }); + }); - it('list: IS_EMPTY action', function test() { - return this - .filteredListRequest({ lastName: { isempty: true } }) - .then((result) => { - assert(result); - expect(result.users).to.have.length(0); - }); - }); + it('list: IS_EMPTY action', function test() { + return this + .filteredListRequest({ lastName: { isempty: true } }) + .then((result) => { + assert(result); + expect(result.users).to.have.length(0); + }); + }); - it('list: EXISTS action', function test() { - return this - .filteredListRequest({ lastName: { exists: true } }) - .then((result) => { - assert(result); - expect(result.users).to.have.length.gte(1); - }); - }); + it('list: EXISTS action', function test() { + return this + .filteredListRequest({ lastName: { exists: true } }) + .then((result) => { + assert(result); + expect(result.users).to.have.length.gte(1); + }); + }); - it('list by id', function test() { - // -@id:{$f_id_ne} PARAMS 2 f_id_ne unknown - return this - .users - .dispatch('list', { - params: { - offset: 0, - limit: 3, - criteria: 'id', // sort by - audience: this.audience, - filter: { - '#': { ne: 'unknown' }, + it('list by id', function test() { + // -@id:{$f_id_ne} PARAMS 2 f_id_ne unknown + return this + .users + .dispatch('list', { + params: { + offset: 0, + limit: 3, + criteria: 'id', // sort by + audience: this.audience, + filter: { + '#': { ne: 'unknown' }, + }, }, - }, - }) - .then((result) => { - assert(result); - expect(result.users).to.have.length(3); - - result.users.forEach((user) => { - expect(user).to.have.ownProperty('id'); + }) + .then((result) => { + assert(result); + expect(result.users).to.have.length(3); + + result.users.forEach((user) => { + expect(user).to.have.ownProperty('id'); + }); }); - }); - }); + }); - it('use custom audience', function test() { - // FT.SEARCH {ms-users}-test-api-idx @level:[-inf 40] - return this - .users - .dispatch('list', { - params: { - criteria: 'level', - audience: TEST_AUDIENCE, - filter: { - level: { lte: 30 }, + it('use custom audience', function test() { + // FT.SEARCH {ms-users}-test-api-idx @level:[-inf 40] + return this + .users + .dispatch('list', { + params: { + criteria: 'level', + audience: TEST_AUDIENCE, + filter: { + level: { lte: 30 }, + }, }, - }, - }) - .then((result) => { - expect(result.users).to.have.length(3); - expect(result.users.length); - - result.users.forEach((user) => { - expect(user).to.have.ownProperty('id'); - expect(user).to.have.ownProperty('metadata'); - - const data = user.metadata[TEST_AUDIENCE]; - expect(data).to.have.ownProperty('email'); - expect(data.email.endsWith('.org')); - - expect(data).to.have.ownProperty('level'); - expect(data.level).to.be.lte(30); // 10 20 30 + }) + .then((result) => { + expect(result.users).to.have.length(3); + expect(result.users.length); + + result.users.forEach((user) => { + expect(user).to.have.ownProperty('id'); + expect(user).to.have.ownProperty('metadata'); + + const data = user.metadata[TEST_AUDIENCE]; + expect(data).to.have.ownProperty('email'); + expect(data.email.endsWith('.org')); + + expect(data).to.have.ownProperty('level'); + expect(data.level).to.be.lte(30); // 10 20 30 + }); }); - }); + }); }); -}); +} diff --git a/test/suites/actions/list.js b/test/suites/actions/list.js index ff2d35bb9..7d8cd8b0f 100644 --- a/test/suites/actions/list.js +++ b/test/suites/actions/list.js @@ -11,6 +11,7 @@ for (const redisSearchEnabled of [false, true]) { // testing in two mode const ctx = { redisSearch: { enabled: redisSearchEnabled, + queryMethod: 'search', }, redisIndexDefinitions, }; From 50dc82d0c6c639f00370d365ebef01e1eb5085e2 Mon Sep 17 00:00:00 2001 From: sky3d Date: Thu, 15 Dec 2022 16:53:16 +0300 Subject: [PATCH 2/2] fix: restore active user index name --- src/actions/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/list.js b/src/actions/list.js index 7d83fa0a9..421ef5a4d 100644 --- a/src/actions/list.js +++ b/src/actions/list.js @@ -172,7 +172,7 @@ module.exports = function iterateOverActiveUsers({ params }) { } else if (params.public === undefined || params.public === false) { index = USERS_INDEX; } else { - index = `${USERS_REFERRAL_INDEX}: ${params.public}`; + index = `${USERS_REFERRAL_INDEX}:${params.public}`; } const ctx = {