api: share one pagination helper across the list services
The mood, sleep-record, trigger, and care-action lists each recomputed skip, totalPages, and nextPage from page and limit, so the arithmetic could drift apart unnoticed. paginate holds it once in calc.ts and the response objects stay byte-identical. The count now runs before the page query, where the two used to run together, because the helper needs the total to resolve skip
Pedro Lucas Porcellis porcellis@eletrotupi.com · 10 days ago
fe4f9989edbeafd44d768c2cccbaa35c1af09360 Browse filesParents
074b881Changed files
6
Additions
+84
Deletions
-56
api/src/services/careAction.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 +
import { paginate } from '@app/utils/calc';
2 3
import { CareAction, CareActionType } from '@prisma/client';
3 4
import { CreateCareActionInput, PatchCareActionInput } from '@app/schemas';
4 5
import { syncMedicineReminderJobs } from '@app/services/medicineReminder.sync';
5 6
6 7
const careActionIncludes = {
17 18
to?: string;
18 19
page?: number;
19 20
limit?: number;
20 21
},
21 22
) => {
22 -
const page = params?.page ?? 1;
23 23
const limit = params?.limit ?? 20;
24 -
const skip = (page - 1) * limit;
25 24
26 25
const where = {
27 26
userId,
28 27
...(params?.type ? { type: params.type } : {}),
29 28
...(params?.from || params?.to
34 33
},
35 34
}
36 35
: {}),
37 36
};
38 37
39 -
const [entries, total] = await Promise.all([
40 -
prisma.careAction.findMany({
41 -
where,
42 -
include: careActionIncludes,
43 -
orderBy: { moment: 'desc' },
44 -
take: limit,
45 -
skip,
46 -
}),
47 -
48 -
prisma.careAction.count({ where }),
49 -
]);
38 +
const total = await prisma.careAction.count({ where });
39 +
const { page, skip, nextPage } = paginate(params?.page ?? 1, limit, total);
50 40
51 -
const totalPages = Math.ceil(total / limit);
41 +
const entries = await prisma.careAction.findMany({
42 +
where,
43 +
include: careActionIncludes,
44 +
orderBy: { moment: 'desc' },
45 +
take: limit,
46 +
skip,
47 +
});
52 48
53 49
return {
54 50
entries,
55 51
total,
56 52
page,
57 -
nextPage: page < totalPages ? page + 1 : null,
53 +
nextPage,
58 54
};
59 55
};
60 56
61 57
export const getCareActionById = async (userId: number, id: number) => {
62 58
return prisma.careAction.findUnique({
227 223
moodId: input.moodId,
228 224
},
229 225
include: careActionIncludes,
230 226
});
231 227
};
api/src/services/mood.service.ts
1 1
import { prisma } from "@app/lib/prisma";
2 2
import { Mood } from "@prisma/client";
3 3
import {
4 4
rollingPeriod,
5 5
appDayPeriod,
6 6
} from "@app/lib/queue/processors/insights/utils";
7 7
import { InsightJobName } from "@app/lib/queue/types";
8 8
import { getQueue } from "@app/lib/queue";
9 +
import { paginate } from "@app/utils/calc";
9 10
10 11
import { CreateMoodInput } from "@app/schemas";
11 12
12 13
export const createMood = async (
13 14
userId: number,
43 44
entries: Mood[];
44 45
total: number;
45 46
page: number;
46 47
nextPage: number | null;
47 48
}> => {
48 -
const page = params?.page ?? 1;
49 49
const limit = params?.limit ?? 20;
50 -
const skip = (page - 1) * limit;
51 50
52 51
const where = {
53 52
userId,
54 53
...(params?.from || params?.to
55 54
? {
59 58
},
60 59
}
61 60
: {}),
62 61
};
63 62
64 -
const [entries, total] = await Promise.all([
65 -
prisma.mood.findMany({
66 -
where,
67 -
include: { moodComponents: true },
68 -
orderBy: { moment: "desc" },
69 -
take: limit,
70 -
skip,
71 -
}),
72 -
prisma.mood.count({ where }),
73 -
]);
63 +
const total = await prisma.mood.count({ where });
64 +
const { page, skip, nextPage } = paginate(params?.page ?? 1, limit, total);
74 65
75 -
const totalPages = Math.ceil(total / limit);
66 +
const entries = await prisma.mood.findMany({
67 +
where,
68 +
include: { moodComponents: true },
69 +
orderBy: { moment: "desc" },
70 +
take: limit,
71 +
skip,
72 +
});
76 73
77 74
return {
78 75
entries,
79 76
total,
80 77
page,
81 -
nextPage: page < totalPages ? page + 1 : null,
78 +
nextPage,
82 79
};
83 80
};
84 81
85 82
export const getMoodById = async (id: number) => {
86 83
return await prisma.mood.findUnique({
152 149
removeOnFail: true,
153 150
},
154 151
},
155 152
]);
156 153
};
api/src/services/sleepRecord.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 2
import { SleepRecord } from '@prisma/client';
3 3
4 4
import { CreateSleepRecordInput, UpdateSleepRecordInput } from '@app/schemas';
5 5
import {
6 6
rollingPeriod,
7 7
appDayPeriod,
8 8
} from "@app/lib/queue/processors/insights/utils";
9 9
import { InsightJobName } from "@app/lib/queue/types";
10 10
import { getQueue } from "@app/lib/queue";
11 +
import { paginate } from '@app/utils/calc';
11 12
12 13
export const createSleepRecord = async (
13 14
userId: number, input: CreateSleepRecordInput
14 15
): Promise<SleepRecord> => {
15 16
const sleepRecord = await prisma.sleepRecord.create({
31 32
page?: number;
32 33
from?: string;
33 34
to?: string;
34 35
}
35 36
): Promise<{ entries: SleepRecord[]; total: number; page: number; nextPage: number | null }> => {
36 -
const page = params?.page ?? 1;
37 37
const limit = params?.limit ?? 20;
38 -
const skip = (page - 1) * limit;
39 38
40 39
const where = {
41 40
userId,
42 41
...(params?.from || params?.to ? {
43 42
date: {
45 44
...(params.to && { lte: new Date(params.to) }),
46 45
}
47 46
} : {}),
48 47
};
49 48
50 -
const [entries, total] = await Promise.all([
51 -
prisma.sleepRecord.findMany({
52 -
where,
53 -
orderBy: { date: 'desc' },
54 -
take: limit,
55 -
skip,
56 -
}),
57 -
prisma.sleepRecord.count({ where }),
58 -
]);
49 +
const total = await prisma.sleepRecord.count({ where });
50 +
const { page, skip, nextPage } = paginate(params?.page ?? 1, limit, total);
59 51
60 -
const totalPages = Math.ceil(total / limit);
52 +
const entries = await prisma.sleepRecord.findMany({
53 +
where,
54 +
orderBy: { date: 'desc' },
55 +
take: limit,
56 +
skip,
57 +
});
61 58
62 59
return {
63 60
entries,
64 61
total,
65 62
page,
66 -
nextPage: page < totalPages ? page + 1 : null,
63 +
nextPage,
67 64
};
68 65
};
69 66
70 67
export const destroySleepRecordById = async (
71 68
userId: number, id: number
124 121
removeOnFail: true,
125 122
},
126 123
},
127 124
]);
128 125
}
api/src/services/trigger.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 2
import { Trigger } from '@prisma/client';
3 3
4 4
import { CreateTriggerInput, UpdateTriggerInput } from '@app/schemas';
5 5
import { rollingPeriod } from "@app/lib/queue/processors/insights/utils";
6 6
import { InsightJobName } from "@app/lib/queue/types";
7 7
import { getQueue } from "@app/lib/queue";
8 +
import { paginate } from '@app/utils/calc';
8 9
9 10
export const createTrigger = async (
10 11
userId: number, input: CreateTriggerInput
11 12
): Promise<Trigger> => {
12 13
const trigger = await prisma.trigger.create({
28 29
page?: number;
29 30
from?: string;
30 31
to?: string;
31 32
}
32 33
): Promise<{ entries: Trigger[]; total: number; page: number; nextPage: number | null }> => {
33 -
const page = params?.page ?? 1;
34 34
const limit = params?.limit ?? 20;
35 -
const skip = (page - 1) * limit;
36 35
37 36
const where = {
38 37
userId,
39 38
...(params?.from || params?.to ? {
40 39
moment: {
42 41
...(params.to && { lte: new Date(params.to) }),
43 42
}
44 43
} : {}),
45 44
};
46 45
47 -
const [entries, total] = await Promise.all([
48 -
prisma.trigger.findMany({
49 -
where,
50 -
orderBy: { moment: 'desc' },
51 -
take: limit,
52 -
skip,
53 -
}),
54 -
prisma.trigger.count({ where }),
55 -
]);
46 +
const total = await prisma.trigger.count({ where });
47 +
const { page, skip, nextPage } = paginate(params?.page ?? 1, limit, total);
56 48
57 -
const totalPages = Math.ceil(total / limit);
49 +
const entries = await prisma.trigger.findMany({
50 +
where,
51 +
orderBy: { moment: 'desc' },
52 +
take: limit,
53 +
skip,
54 +
});
58 55
59 56
return {
60 57
entries,
61 58
total,
62 59
page,
63 -
nextPage: page < totalPages ? page + 1 : null,
60 +
nextPage,
64 61
};
65 62
};
66 63
67 64
export const destroyTriggerById = async (
68 65
userId: number, id: number
124 121
removeOnComplete: true,
125 122
removeOnFail: true,
126 123
}
127 124
);
128 125
}
api/src/utils/calc.ts
1 1
// TODO: Move other calculations here
2 2
export const avg = (values: number[]) =>
3 3
values.reduce((a, b) => a + b, 0) / values.length;
4 4
5 5
export const formatHours = (hours: number): string => {
26 26
if (h === 0) return `${sign}${m}min`;
27 27
if (m === 0) return `${sign}${h}h`;
28 28
29 29
return `${sign}${h}h ${m}min`;
30 30
}
31 +
32 +
export const paginate = (
33 +
page: number,
34 +
limit: number,
35 +
total: number,
36 +
): { page: number; skip: number; nextPage: number | null } => {
37 +
const skip = (page - 1) * limit;
38 +
const totalPages = Math.ceil(total / limit);
39 +
40 +
return {
41 +
page,
42 +
skip,
43 +
nextPage: page < totalPages ? page + 1 : null,
44 +
};
45 +
}
api/tests/utils/calc.unit.test.ts
1 1
import { describe, it, expect } from 'vitest'
2 -
import { avg, formatHours, formatDiff } from '@app/utils/calc'
2 +
import { avg, formatHours, formatDiff, paginate } from '@app/utils/calc'
3 3
4 4
describe('avg', () => {
5 5
it('averages a list of numbers', () => {
6 6
expect(avg([2, 4, 6])).toBe(4)
7 7
})
48 48
49 49
it('renders a negative sub-hour difference in minutes only', () => {
50 50
expect(formatDiff(-30)).toBe('-30min')
51 51
})
52 52
})
53 +
54 +
describe('paginate', () => {
55 +
it('points the first page at the next one', () => {
56 +
expect(paginate(1, 10, 25)).toEqual({ page: 1, skip: 0, nextPage: 2 })
57 +
})
58 +
59 +
it('points a middle page at the next one', () => {
60 +
expect(paginate(2, 10, 25)).toEqual({ page: 2, skip: 10, nextPage: 3 })
61 +
})
62 +
63 +
it('closes the chain on the last page', () => {
64 +
expect(paginate(3, 10, 25)).toEqual({ page: 3, skip: 20, nextPage: null })
65 +
})
66 +
67 +
it('has no next page past the end', () => {
68 +
expect(paginate(4, 10, 25)).toEqual({ page: 4, skip: 30, nextPage: null })
69 +
})
70 +
71 +
it('treats an exact division as the last page', () => {
72 +
expect(paginate(2, 10, 20)).toEqual({ page: 2, skip: 10, nextPage: null })
73 +
})
74 +
75 +
it('has no next page for an empty result set', () => {
76 +
expect(paginate(1, 10, 0)).toEqual({ page: 1, skip: 0, nextPage: null })
77 +
})
78 +
})