Skip to content

Commit d7d9a8c

Browse files
authored
chore: pipe sorting functionality to handle missing fields (RocketChat#36444)
1 parent 0de9318 commit d7d9a8c

2 files changed

Lines changed: 72 additions & 23 deletions

File tree

apps/meteor/client/lib/cachedCollections/pipe.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,36 @@ describe('pipe', () => {
1616
{ _id: 5, name: 'Eve', ts: 40, date: new Date('2025-07-16T05:00:00.000Z') },
1717
];
1818

19+
describe('missing fields', () => {
20+
it('should let the rows with missing fields at the end', () => {
21+
const arr = [...sampleData];
22+
arr.splice(Math.floor(Math.random() * arr.length), 0, { _id: 6, name: 'A', ts: 50 } as ITestData);
23+
24+
const result = pipe<ITestData>().sortByField('date', 1).apply(arr);
25+
expect(result).toEqual([...sampleData, { _id: 6, name: 'A', ts: 50 }]);
26+
});
27+
it('should sort by fallback fields if a field is missing', () => {
28+
// adds randomly an row if missing date
29+
const arr = [...sampleData];
30+
arr.splice(Math.floor(Math.random() * arr.length), 0, { _id: 6, name: 'A', ts: 50 } as ITestData);
31+
arr.splice(Math.floor(Math.random() * arr.length), 0, { _id: 7, name: 'B', ts: 50 } as ITestData);
32+
arr.splice(Math.floor(Math.random() * arr.length), 0, { _id: 8, name: 'C', ts: 50 } as ITestData);
33+
34+
const result = pipe<ITestData>().sortByField('date', 1, ['name']).apply(arr);
35+
expect(result).toEqual([...sampleData, { _id: 6, name: 'A', ts: 50 }, { _id: 7, name: 'B', ts: 50 }, { _id: 8, name: 'C', ts: 50 }]);
36+
});
37+
it('should sort by fallback fields if a field is missing with second signature', () => {
38+
// adds randomly an row if missing date
39+
const arr = [...sampleData];
40+
arr.splice(Math.floor(Math.random() * arr.length), 0, { _id: 6, name: 'A', ts: 50 } as ITestData);
41+
arr.splice(Math.floor(Math.random() * arr.length), 0, { _id: 7, name: 'B', ts: 50 } as ITestData);
42+
arr.splice(Math.floor(Math.random() * arr.length), 0, { _id: 8, name: 'C', ts: 50 } as ITestData);
43+
44+
const result = pipe<ITestData>().sortByField(['date', 'name'], 1).apply(arr);
45+
expect(result).toEqual([...sampleData, { _id: 6, name: 'A', ts: 50 }, { _id: 7, name: 'B', ts: 50 }, { _id: 8, name: 'C', ts: 50 }]);
46+
});
47+
});
48+
1949
it('should correctly slice the array with skip and limit', () => {
2050
const result = pipe<ITestData>().slice(1, 2).apply(sampleData);
2151
expect(result).toEqual([

apps/meteor/client/lib/cachedCollections/pipe.ts

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
interface IPipeReturn<D> {
22
slice(skip: number, limit: number): IPipeReturn<D>;
3-
sortByField(fieldName: keyof D, direction?: 1 | -1): IPipeReturn<D>;
3+
sortByField(fieldName: keyof D, direction?: 1 | -1, fallback?: Array<keyof D>): IPipeReturn<D>;
4+
sortByField(fieldName: Array<keyof D>, direction?: 1 | -1, fallback?: Array<keyof D>): IPipeReturn<D>;
45
apply(): D[];
56
pipe(p: IPipeReturn<D>): IPipeReturn<D>;
67
}
78

89
interface IPipeReturnNoInitialData<D> {
910
slice(skip: number, limit: number): IPipeReturnNoInitialData<D>;
10-
sortByField(fieldName: keyof D, direction?: 1 | -1): IPipeReturnNoInitialData<D>;
11+
sortByField(fieldName: keyof D, direction?: 1 | -1, fallback?: Array<keyof D>): IPipeReturnNoInitialData<D>;
12+
sortByField(fieldName: Array<keyof D>, direction?: 1 | -1): IPipeReturnNoInitialData<D>;
1113
apply(arg: D[]): D[];
1214
pipe(p: IPipeReturnNoInitialData<D>): IPipeReturnNoInitialData<D>;
1315
}
@@ -35,29 +37,46 @@ export function pipe<D>(
3537
);
3638
},
3739

38-
sortByField(fieldName: keyof D, direction: 1 | -1 = 1) {
39-
return pipe<D>(
40-
initialData,
41-
merge(
42-
(arr: D[]) =>
43-
[...arr].sort((a, b) => {
44-
const aValue = a[fieldName];
45-
const bValue = b[fieldName];
40+
sortByField(fieldName: keyof D | Array<keyof D>, direction: 1 | -1 = 1, fallback: Array<keyof D> = []) {
41+
if (Array.isArray(fieldName)) {
42+
return this.sortByField(fieldName[0], direction, fieldName.slice(1));
43+
}
44+
45+
const sort = (fieldName: keyof D, direction: 1 | -1, fallback: Array<keyof D>) => {
46+
return (a: D, b: D): number => {
47+
const aValue = a[fieldName];
48+
const bValue = b[fieldName];
49+
50+
if (aValue === undefined && bValue === undefined && fallback.length > 0) {
51+
const [field, ...rest] = fallback;
52+
return sort(field, direction, rest)(a, b);
53+
}
54+
55+
if (aValue === undefined) {
56+
return 1;
57+
}
4658

47-
if (aValue instanceof Date && bValue instanceof Date) {
48-
return direction * (aValue.getTime() - bValue.getTime());
49-
}
59+
if (bValue === undefined) {
60+
return -1;
61+
}
5062

51-
if (typeof aValue === 'string' && typeof bValue === 'string') {
52-
return direction * aValue.localeCompare(bValue);
53-
}
54-
if (typeof aValue === 'number' && typeof bValue === 'number') {
55-
return direction * (aValue - bValue);
56-
}
57-
return 0;
58-
}),
59-
acc,
60-
),
63+
if (aValue instanceof Date && bValue instanceof Date) {
64+
return direction * (aValue.getTime() - bValue.getTime());
65+
}
66+
67+
if (typeof aValue === 'string' && typeof bValue === 'string') {
68+
return direction * aValue.localeCompare(bValue);
69+
}
70+
if (typeof aValue === 'number' && typeof bValue === 'number') {
71+
return direction * (aValue - bValue);
72+
}
73+
return 0;
74+
};
75+
};
76+
77+
return pipe<D>(
78+
initialData,
79+
merge((arr: D[]) => [...arr].sort(sort(fieldName, direction, fallback)), acc),
6180
);
6281
},
6382

0 commit comments

Comments
 (0)