Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CHAT-1814] Updates to message search #677

Merged
merged 19 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
BanUserOptions,
ChannelAPIResponse,
ChannelData,
ChannelFilters,
ChannelMemberAPIResponse,
ChannelMemberResponse,
ChannelQueryOptions,
Expand All @@ -33,6 +34,8 @@ import {
QueryMembersOptions,
Reaction,
ReactionAPIResponse,
SearchOptions,
SearchPayload,
SearchAPIResponse,
SendMessageAPIResponse,
TruncateChannelAPIResponse,
Expand All @@ -41,6 +44,7 @@ import {
UserFilters,
UserResponse,
UserSort,
SearchMessageSort,
} from './types';
import { Role } from './permissions';

Expand Down Expand Up @@ -299,7 +303,7 @@ export class Channel<
* search - Query messages
*
* @param {MessageFilters<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType> | string} query search query or object MongoDB style filters
* @param {{client_id?: string; connection_id?: string; limit?: number; offset?: number; query?: string; message_filter_conditions?: MessageFilters<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType>}} options Option object, {user_id: 'tommaso'}
* @param {{client_id?: string; connection_id?: string; query?: string; message_filter_conditions?: MessageFilters<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType>}} options Option object, {user_id: 'tommaso'}
*
* @return {Promise<SearchAPIResponse<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType>>} search messages response
*/
Expand All @@ -314,10 +318,9 @@ export class Channel<
UserType
>
| string,
options: {
options: SearchOptions<MessageType> & {
client_id?: string;
connection_id?: string;
limit?: number;
message_filter_conditions?: MessageFilters<
AttachmentType,
ChannelType,
Expand All @@ -326,14 +329,28 @@ export class Channel<
ReactionType,
UserType
>;
offset?: number;
query?: string;
} = {},
) {
if ('offset' in options && ('sort' in options || 'next' in options)) {
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
throw Error(`Cannot specify offset with sort or next parameters`);
}
// Return a list of channels
const payload = {
filter_conditions: { cid: this.cid },
...options,
const { sort: sortValue, ...optionsWithoutSort } = { ...options };
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
const payload: SearchPayload<
AttachmentType,
ChannelType,
CommandType,
MessageType,
ReactionType,
UserType
> = {
filter_conditions: { cid: this.cid } as ChannelFilters<
ChannelType,
CommandType,
UserType
>,
...optionsWithoutSort,
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
};
if (typeof query === 'string') {
payload.query = query;
Expand All @@ -342,7 +359,9 @@ export class Channel<
} else {
throw Error(`Invalid type ${typeof query} for query parameter`);
}

if (sortValue) {
payload.sort = normalizeQuerySort<SearchMessageSort<MessageType>>(sortValue);
}
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
// Make sure we wait for the connect promise if there is a pending one
await this.getClient().wsPromise;

Expand Down
17 changes: 12 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ import {
PermissionAPIResponse,
PermissionsAPIResponse,
ReactionResponse,
SearchAPIResponse,
SearchOptions,
SearchPayload,
SearchAPIResponse,
SendFileAPIResponse,
StreamChatOptions,
TestPushDataInput,
Expand All @@ -102,6 +102,7 @@ import {
UserOptions,
UserResponse,
UserSort,
SearchMessageSort,
} from './types';

function isString(x: unknown): x is string {
Expand Down Expand Up @@ -1761,7 +1762,7 @@ export class StreamChat<
*
* @param {ChannelFilters<ChannelType, CommandType, UserType>} filterConditions MongoDB style filter conditions
* @param {MessageFilters<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType> | string} query search query or object MongoDB style filters
* @param {SearchOptions} [options] Option object, {user_id: 'tommaso'}
* @param {SearchOptions<MessageType>} [options] Option object, {user_id: 'tommaso'}
*
* @return {Promise<SearchAPIResponse<AttachmentType, ChannelType, CommandType, MessageType, ReactionType, UserType>>} search messages response
*/
Expand All @@ -1777,9 +1778,12 @@ export class StreamChat<
ReactionType,
UserType
>,
options: SearchOptions = {},
options: SearchOptions<MessageType> = {},
) {
// Return a list of channels
if ('offset' in options && ('sort' in options || 'next' in options)) {
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
throw Error(`Cannot specify offset with sort or next parameters`);
}
const { sort: sortValue, ...optionsWithoutSort } = { ...options };
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
const payload: SearchPayload<
AttachmentType,
ChannelType,
Expand All @@ -1789,7 +1793,7 @@ export class StreamChat<
UserType
> = {
filter_conditions: filterConditions,
...options,
...optionsWithoutSort,
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
};
if (typeof query === 'string') {
payload.query = query;
Expand All @@ -1798,6 +1802,9 @@ export class StreamChat<
} else {
throw Error(`Invalid type ${typeof query} for query parameter`);
}
if (sortValue) {
payload.sort = normalizeQuerySort<SearchMessageSort<MessageType>>(sortValue);
}
miagilepner marked this conversation as resolved.
Show resolved Hide resolved

// Make sure we wait for the connect promise if there is a pending one
await this.setUserPromise;
Expand Down
46 changes: 43 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,8 +649,17 @@ export type SearchAPIResponse<
UserType
>;
}[];
next?: string;
previous?: string;
results_warning?: SearchWarning;
};

export type SearchWarning = {
channel_search_cids: string[];
channel_search_count: number;
warning_code: number;
warning_description: string;
};
export type SendFileAPIResponse = APIResponse & { file: string };

export type SendMessageAPIResponse<
Expand Down Expand Up @@ -935,9 +944,11 @@ export type QueryMembersOptions = {
user_id_lte?: string;
};

export type SearchOptions = {
export type SearchOptions<MessageType = UnknownType> = {
limit?: number;
next?: string;
offset?: number;
sort?: SearchMessageSort<MessageType>;
};

export type StreamChatOptions = AxiosRequestConfig & {
Expand Down Expand Up @@ -1407,9 +1418,34 @@ export type UserSort<UserType = UnknownType> =
| Sort<UserResponse<UserType>>
| Array<Sort<UserResponse<UserType>>>;

export type QuerySort<ChannelType = UnknownType, UserType = UnknownType> =
export type SearchMessageSortBase<MessageType = UnknownType> = Sort<MessageType> & {
nhannah marked this conversation as resolved.
Show resolved Hide resolved
attachments?: AscDesc;
'attachments.type'?: AscDesc;
created_at?: AscDesc;
id?: AscDesc;
'mentioned_users.id'?: AscDesc;
parent_id?: AscDesc;
pinned?: AscDesc;
relevance?: AscDesc;
reply_count?: AscDesc;
text?: AscDesc;
type?: AscDesc;
updated_at?: AscDesc;
'user.id'?: AscDesc;
};

export type SearchMessageSort<MessageType = UnknownType> =
| SearchMessageSortBase<MessageType>
| Array<SearchMessageSortBase<MessageType>>;

export type QuerySort<
ChannelType = UnknownType,
UserType = UnknownType,
MessageType = UnknownType
> =
| BannedUsersSort
| ChannelSort<ChannelType>
| SearchMessageSort<MessageType>
| UserSort<UserType>;

/**
Expand Down Expand Up @@ -1898,7 +1934,7 @@ export type SearchPayload<
MessageType = UnknownType,
ReactionType = UnknownType,
UserType = UnknownType
> = SearchOptions & {
> = Omit<SearchOptions<MessageType>, 'sort'> & {
client_id?: string;
connection_id?: string;
filter_conditions?: ChannelFilters<ChannelType, CommandType, UserType>;
Expand All @@ -1911,6 +1947,10 @@ export type SearchPayload<
UserType
>;
query?: string;
sort?: Array<{
direction: AscDesc;
field: Extract<keyof SearchMessageSortBase<MessageType>, string>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I missing something here?

keyof SearchMessageSortBase<MessageType> will always be a string, so Extract<keyof SearchMessageSortBase<MessageType>, string> should be the same thing as keyof SearchMessageSortBase<MessageType>? But maybe I am not seeing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyof SearchMessageSortBase<MessageType> is string | number

}>;
};

export type TestPushDataInput = {
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function normalizeQuerySort<T extends QuerySort>(sort: T) {
const sortArr = Array.isArray(sort) ? sort : [sort];
for (const item of sortArr) {
const entries = (Object.entries(item) as unknown) as [
T extends (infer K)[] ? keyof K : keyof T,
T extends (infer K)[] ? Extract<keyof K, string> : Extract<keyof T, string>,
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
AscDesc,
][];
if (entries.length > 1) {
Expand Down
31 changes: 31 additions & 0 deletions test/unit/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,34 @@ describe('event subscription and unsubscription', () => {
expect(channel.listeners['all'].length).to.be.equal(0);
});
});
describe('Channel search', async () => {
const client = await getClientWithUser();
const channel = client.channel('messaging', uuidv4());

it('search with sorting by defined field', async () => {
client.get = (url, config) => {
expect(config.payload.sort).to.be.eql([
{ field: 'updated_at', direction: -1 },
]);
};
await channel.search('query', { sort: [{ updated_at: -1 }] });
});
it('search with sorting by custom field', async () => {
client.get = (url, config) => {
expect(config.payload.sort).to.be.eql([
{ field: 'custom_field', direction: -1 },
]);
};
await channel.search('query', { sort: [{ custom_field: -1 }] });
});
it('sorting and offset fails', async () => {
await expect(
channel.search('query', { offset: 1, sort: [{ custom_field: -1 }] }),
).to.be.rejectedWith(Error);
});
it('next and offset fails', async () => {
await expect(
channel.search('query', { offset: 1, next: 'next' }),
).to.be.rejectedWith(Error);
});
});
44 changes: 43 additions & 1 deletion test/unit/client.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import chai from 'chai';

import chaiAsPromised from 'chai-as-promised';
import { generateMsg } from './test-utils/generateMessage';
import { getClientWithUser } from './test-utils/getClient';

import { StreamChat } from '../../src/client';

const expect = chai.expect;
chai.use(chaiAsPromised);

describe('StreamChat getInstance', () => {
beforeEach(() => {
Expand Down Expand Up @@ -268,3 +269,44 @@ describe('updateMessage should ensure sanity of `mentioned_users`', () => {
);
});
});

describe('Client search', async () => {
const client = await getClientWithUser();

it('search with sorting by defined field', async () => {
client.get = (url, config) => {
expect(config.payload.sort).to.be.eql([
{ field: 'updated_at', direction: -1 },
]);
};
await client.search({ cid: 'messaging:my-cid' }, 'query', {
sort: [{ updated_at: -1 }],
});
});
it('search with sorting by custom field', async () => {
client.get = (url, config) => {
expect(config.payload.sort).to.be.eql([
{ field: 'custom_field', direction: -1 },
]);
};
await client.search({ cid: 'messaging:my-cid' }, 'query', {
sort: [{ custom_field: -1 }],
});
});
it('sorting and offset fails', async () => {
await expect(
client.search({ cid: 'messaging:my-cid' }, 'query', {
offset: 1,
sort: [{ custom_field: -1 }],
}),
).to.be.rejectedWith(Error);
});
it('next and offset fails', async () => {
await expect(
client.search({ cid: 'messaging:my-cid' }, 'query', {
offset: 1,
next: 'next',
}),
).to.be.rejectedWith(Error);
});
});