eletrotupi / tcc/ commit / fe4f998

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 files
Parents 074b881
Changed 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 +
})