Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants';
import type {
AIState,
APIResponse,
AscDesc,
BanUserOptions,
ChannelAPIResponse,
ChannelData,
Expand Down Expand Up @@ -66,6 +65,7 @@ import type {
SendMessageAPIResponse,
SendMessageOptions,
SendReactionOptions,
Sort,
StaticLocationPayload,
TruncateChannelAPIResponse,
TruncateOptions,
Expand Down Expand Up @@ -1305,7 +1305,7 @@ export class Channel {
async getReplies(
parent_id: string,
options: MessagePaginationOptions & { user?: UserResponse; user_id?: string },
sort?: { created_at: AscDesc }[],
sort?: Required<Sort<'created_at'>>[],
) {
const normalizedSort = sort ? normalizeQuerySort(sort) : undefined;
const data = await this.getClient().get<GetRepliesAPIResponse>(
Expand Down
6 changes: 3 additions & 3 deletions src/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
throttle,
} from './utils';
import type {
AscDesc,
EventTypes,
LocalMessage,
MessagePaginationOptions,
MessageResponse,
ReadResponse,
Sort,
ThreadResponse,
UserResponse,
} from './types';
Expand All @@ -22,7 +22,7 @@ import { MessageComposer } from './messageComposer';
import { WithSubscriptions } from './utils/WithSubscriptions';

type QueryRepliesOptions = {
sort?: { created_at: AscDesc }[];
sort?: Required<Sort<'created_at'>>[];
} & MessagePaginationOptions & { user?: UserResponse; user_id?: string };

export type ThreadState = {
Expand Down Expand Up @@ -68,7 +68,7 @@ export type ThreadUserReadState = {
export type ThreadReadState = Record<string, ThreadUserReadState | undefined>;

const DEFAULT_PAGE_LIMIT = 50;
const DEFAULT_SORT: { created_at: AscDesc }[] = [{ created_at: -1 }];
const DEFAULT_SORT: QueryRepliesOptions['sort'] = [{ created_at: -1 }];
const MARK_AS_READ_THROTTLE_TIMEOUT = 1000;
// TODO: remove this once we move to API v2
export const THREAD_RESPONSE_RESERVED_KEYS: Record<keyof ThreadResponse, true> = {
Expand Down
119 changes: 56 additions & 63 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2168,32 +2168,37 @@ export type MemberFilters = QueryFilters<

export type BannedUsersSort = BannedUsersSortBase | Array<BannedUsersSortBase>;

export type BannedUsersSortBase = { created_at?: AscDesc };
export type BannedUsersSortBase = Sort<'created_at'>;

export type ReactionSort = ReactionSortBase | Array<ReactionSortBase>;

export type ReactionSortBase = Sort<CustomReactionData> & {
created_at?: AscDesc;
};
export type ReactionSortBase = Sort<CustomReactionData> & Sort<'created_at'>;

export type ChannelSort = ChannelSortBase | Array<ChannelSortBase>;

export type ChannelSortBase = Sort<CustomChannelData> & {
created_at?: AscDesc;
has_unread?: AscDesc;
last_message_at?: AscDesc;
last_updated?: AscDesc;
member_count?: AscDesc;
pinned_at?: AscDesc;
unread_count?: AscDesc;
updated_at?: AscDesc;
};
export type ChannelSortBase = Sort<CustomChannelData> &
Sort<
| 'created_at'
| 'has_unread'
| 'last_message_at'
| 'last_updated'
| 'member_count'
| 'pinned_at'
| 'unread_count'
| 'updated_at'
>;

export type PinnedMessagesSort = PinnedMessagesSortBase | Array<PinnedMessagesSortBase>;
export type PinnedMessagesSortBase = { pinned_at?: AscDesc };

export type Sort<T> = {
[P in keyof T]?: AscDesc;
export type PinnedMessagesSortBase = Sort<'pinned_at'>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Sort<T extends string | Record<string, any>> = {
[P in T extends string ? T : keyof T]?:
| AscDesc
| {
direction: AscDesc;
type?: 'string' | 'number';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kanat what types will be supported?

};
};

export type UserSort = Sort<UserResponse> | Array<Sort<UserResponse>>;
Expand All @@ -2212,51 +2217,40 @@ export type MemberSort =
>
>;

export type SearchMessageSortBase = Sort<CustomMessageData> & {
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 SearchMessageSortBase = Sort<CustomMessageData> &
Sort<
| 'attachments'
| 'attachments.type'
| 'created_at'
| 'id'
| 'mentioned_users.id'
| 'parent_id'
| 'pinned'
| 'relevance'
| 'reply_count'
| 'text'
| 'type'
| 'updated_at'
| 'user.id'
>;

export type SearchMessageSort = SearchMessageSortBase | Array<SearchMessageSortBase>;

export type QuerySort = BannedUsersSort | ChannelSort | SearchMessageSort | UserSort;

export type DraftSortBase = {
created_at?: AscDesc;
};
export type DraftSortBase = Sort<'created_ats'>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
export type DraftSortBase = Sort<'created_ats'>;
export type DraftSortBase = Sort<'created_at'>;


export type DraftSort = DraftSortBase | Array<DraftSortBase>;

export type PollSort = PollSortBase | Array<PollSortBase>;

export type PollSortBase = {
created_at?: AscDesc;
id?: AscDesc;
is_closed?: AscDesc;
name?: AscDesc;
updated_at?: AscDesc;
};
export type PollSortBase = Sort<
'created_at' | 'id' | 'is_closed' | 'name' | 'updated_at'
>;

export type VoteSort = VoteSortBase | Array<VoteSortBase>;

export type VoteSortBase = {
created_at?: AscDesc;
id?: AscDesc;
is_closed?: AscDesc;
name?: AscDesc;
updated_at?: AscDesc;
};
export type VoteSortBase = PollSortBase;

/**
* Base Types
Expand Down Expand Up @@ -3569,10 +3563,9 @@ export type QueryMessageHistorySort =
| QueryMessageHistorySortBase
| Array<QueryMessageHistorySortBase>;

export type QueryMessageHistorySortBase = {
message_updated_at?: AscDesc;
message_updated_by_id?: AscDesc;
};
export type QueryMessageHistorySortBase = Sort<
'message_updated_at' | 'message_updated_by_id'
>;

export type QueryMessageHistoryOptions = Pager;

Expand Down Expand Up @@ -4321,15 +4314,15 @@ export type LiveLocationPayload = {

export type ThreadSort = ThreadSortBase | Array<ThreadSortBase>;

export type ThreadSortBase = {
active_participant_count?: AscDesc;
created_at?: AscDesc;
last_message_at?: AscDesc;
parent_message_id?: AscDesc;
participant_count?: AscDesc;
reply_count?: AscDesc;
updated_at?: AscDesc;
};
export type ThreadSortBase = Sort<
| 'active_participant_count'
| 'created_at'
| 'last_message_at'
| 'parent_message_id'
| 'participant_count'
| 'reply_count'
| 'updated_at'
>;

export type ThreadFilters = QueryFilters<
{
Expand Down
35 changes: 21 additions & 14 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
PromoteChannelParams,
QueryChannelAPIResponse,
ReactionGroupResponse,
Sort,
UpdatedMessage,
UserResponse,
} from './types';
Expand Down Expand Up @@ -131,20 +132,26 @@ export function addFileToFormData(

return data;
}
export function normalizeQuerySort<T extends Record<string, AscDesc | undefined>>(
sort: T | T[],
) {
const sortFields: Array<{ direction: AscDesc; field: keyof T }> = [];
const sortArr = Array.isArray(sort) ? sort : [sort];
for (const item of sortArr) {
const entries = Object.entries(item) as [keyof T, AscDesc][];
if (entries.length > 1) {
console.warn(
"client._buildSort() - multiple fields in a single sort object detected. Object's field order is not guaranteed",
);
}
for (const [field, direction] of entries) {
sortFields.push({ field, direction });

export function normalizeQuerySort<T extends Sort<string>>(sort: T | T[]) {
const sortFields: Array<{ direction: AscDesc; field: keyof T; type: string | null }> =
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const sortFields: Array<{ direction: AscDesc; field: keyof T; type: string | null }> =
const sortFields: Array<{ direction: AscDesc; field: keyof T; type: string | number | null }> =

Not sure, but basing it on this:

https://github.com/GetStream/stream-chat-js/pull/1673/changes#r2664806559

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The type is just a string in this case as it's supposed to be sent to the BE, TS should not remove this during compile time. In this case it's just generalized to string so I don't have to do "string" | "number" | "some-other-type-once-added" since we don't really care about specificity at that point.

[];
const sortArray = Array.isArray(sort) ? sort : [sort];
for (const item of sortArray) {
const entries = Object.entries(item);

for (const [field, directionOrObject] of entries) {
if (!directionOrObject) continue;

if (typeof directionOrObject === 'number') {
sortFields.push({ field, direction: directionOrObject, type: null });
} else {
sortFields.push({
field,
direction: directionOrObject.direction,
type: directionOrObject.type ?? null,
});
}
}
}
return sortFields;
Expand Down
8 changes: 6 additions & 2 deletions test/unit/channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1753,13 +1753,17 @@ describe('Channel search', async () => {

it('search with sorting by defined field', async () => {
client.get = (url, config) => {
expect(config.payload.sort).to.be.eql([{ field: 'updated_at', direction: -1 }]);
expect(config.payload.sort).toEqual([
{ field: 'updated_at', direction: -1, type: null },
]);
};
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 }]);
expect(config.payload.sort).toEqual([
{ field: 'custom_field', direction: -1, type: null },
]);
};
await channel.search('query', { sort: [{ custom_field: -1 }] });
});
Expand Down
10 changes: 7 additions & 3 deletions test/unit/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,15 +518,19 @@ describe('Client search', async () => {

it('search with sorting by defined field', async () => {
client.get = (url, config) => {
expect(config.payload.sort).to.be.eql([{ field: 'updated_at', direction: -1 }]);
expect(config.payload.sort).toEqual([
{ field: 'updated_at', direction: -1, type: null },
]);
};
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 }]);
expect(config.payload.sort).toEqual([
{ field: 'custom_field', direction: -1, type: null },
]);
};
await client.search({ cid: 'messaging:my-cid' }, 'query', {
sort: [{ custom_field: -1 }],
Expand All @@ -536,7 +540,7 @@ describe('Client search', async () => {
await expect(
client.search({ cid: 'messaging:my-cid' }, 'query', {
offset: 1,
sort: [{ custom_field: -1 }],
sort: [{ custom_field: -1, type: null }],
}),
).resolves.toEqual();
});
Expand Down
2 changes: 1 addition & 1 deletion test/unit/reminders/reminder.api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ describe('Reminder', () => {
channel_cid: 'messaging:123',
remind_at: { $gt: '2024-01-01T00:00:00.000Z' },
},
sort: [{ field: 'remind_at', direction: 1 }],
sort: [{ field: 'remind_at', direction: 1, type: null }],
limit: 10,
});
expect(result).toEqual(queryResponse);
Expand Down
24 changes: 24 additions & 0 deletions test/unit/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,36 @@ describe('test if sort is deterministic', () => {
expect(sort).to.have.length(4);
expect(sort[0].field).to.be.equal('created_at');
expect(sort[0].direction).to.be.equal(1);
expect(sort[0].type).toBe(null);
expect(sort[1].field).to.be.equal('has_unread');
expect(sort[1].direction).to.be.equal(-1);
expect(sort[1].type).toBe(null);
expect(sort[2].field).to.be.equal('last_active');
expect(sort[2].direction).to.be.equal(1);
expect(sort[2].type).toBe(null);
expect(sort[3].field).to.be.equal('deleted_at');
expect(sort[3].direction).to.be.equal(-1);
expect(sort[3].type).toBe(null);
});
it('test sort array with typed fields', () => {
let sort = normalizeQuerySort([
{ created_at: { direction: -1, type: 'string' }, has_unread: -1 },
{ last_active: 1, deleted_at: -1 },
]);

expect(sort).to.have.length(4);
expect(sort[0].field).to.be.equal('created_at');
expect(sort[0].direction).to.be.equal(-1);
expect(sort[0].type).toBe('string');
expect(sort[1].field).to.be.equal('has_unread');
expect(sort[1].direction).to.be.equal(-1);
expect(sort[1].type).toBe(null);
expect(sort[2].field).to.be.equal('last_active');
expect(sort[2].direction).to.be.equal(1);
expect(sort[2].type).toBe(null);
expect(sort[3].field).to.be.equal('deleted_at');
expect(sort[3].direction).to.be.equal(-1);
expect(sort[3].type).toBe(null);
});
});

Expand Down