api: add a controller for fetching insights
Parents:
a772f946 file(s) changed
- api/src/controllers/insights.ts +33 -0
- api/src/createApp.ts +16 -12
- api/src/routes/insights.ts +9 -0
- api/src/schemas/index.ts +15 -12
- api/src/schemas/insight.schema.ts +10 -0
- api/src/services/insight.service.ts +31 -0
api/src/controllers/insights.ts
@@ -0,0 +1,33 @@
1 + import { Response, NextFunction } from "express";
2 + import { AuthenticatedRequest } from "@app/middleware/auth";
3 + import { getInsightsByUserId } from "@app/services/insight.service";
4 + import { InsightsQuerySchema } from "@app/schemas";
5 +
6 + export const InsightsController = {
7 + index: async (
8 + req: AuthenticatedRequest,
9 + res: Response,
10 + next: NextFunction,
11 + ) => {
12 + try {
13 + const parsedQuery = InsightsQuerySchema.safeParse(req.query);
14 +
15 + if (!parsedQuery.success) {
16 + return res.status(400).json({
17 + message: "Invalid query parameters",
18 + errors: parsedQuery.error.flatten(),
19 + });
20 + }
21 +
22 + const insights = await getInsightsByUserId(req.userId!, {
23 + type: parsedQuery.data.type,
24 + period: parsedQuery.data.period,
25 + limit: parsedQuery.data.limit,
26 + });
27 +
28 + return res.status(200).json({ insights });
29 + } catch (err) {
30 + next(err);
31 + }
32 + },
33 + };
api/src/createApp.ts
1 1
import express from "express";
2 2
import cors from "cors";
3 3
import bodyParser from "body-parser";
4 4
import morgan from "morgan";
5 5
import mainRouter from "@app/routes/main";
▸ 20 unchanged lines
26 26
const app = express();
27 27
bootWorkers();
28 28
scheduleInsights();
29 29
30 30
// Configure CORS
31 -
app.use(cors({
32 -
origin: process.env.FRONTEND_URL || '*',
33 -
credentials: true
34 -
}));
31 +
app.use(
32 +
cors({
33 +
origin: process.env.FRONTEND_URL || "*",
34 +
credentials: true,
35 +
}),
36 +
);
35 37
36 38
app.use(bodyParser.json());
37 39
38 -
const isDev = process.env.NODE_ENV !== 'production';
40 +
const isDev = process.env.NODE_ENV !== "production";
39 41
40 42
const logger = pino({
41 -
level: process.env.LOG_LEVEL || 'info',
43 +
level: process.env.LOG_LEVEL || "info",
42 44
...(isDev && {
43 45
transport: {
44 -
target: 'pino-pretty',
46 +
target: "pino-pretty",
45 47
options: {
46 48
colorize: true,
47 49
singleLine: false,
48 -
messageFormat: '{if levelLabel}{levelLabel} - {end}{msg}',
50 +
messageFormat: "{if levelLabel}{levelLabel} - {end}{msg}",
49 51
},
50 52
},
51 53
}),
52 54
});
53 55
▸ 3 unchanged lines
57 59
app.use("/users", userRouter);
58 60
app.use("/auth", authRouter);
59 61
app.use("/moods", moodRouter);
60 62
app.use("/sleep_records", sleepRecordRouter);
61 63
app.use("/triggers", triggerRouter);
64 +
app.use("/insights", insightRouter);
62 65
63 66
app.use(errorHandler); // always last
64 67
65 68
// TODO: flesh the actual page here
66 -
app.use(express.static(path.join(__dirname, './static')));
69 +
app.use(express.static(path.join(__dirname, "./static")));
67 70
app.get(/.*/, (req, res) => {
68 -
res.sendFile(path.join(__dirname, './static/index.html')); });
71 +
res.sendFile(path.join(__dirname, "./static/index.html"));
72 +
});
69 73
70 -
process.on('SIGTERM', shutdown);
71 -
process.on('SIGINT', shutdown);
74 +
process.on("SIGTERM", shutdown);
75 +
process.on("SIGINT", shutdown);
72 76
73 77
return app;
74 78
}
75 79
76 80
const shutdown = async () => {
77 81
await Promise.all([closeAllWorkers(), closeAllQueues()]);
78 82
process.exit(0);
79 83
};
api/src/routes/insights.ts
@@ -0,0 +1,9 @@
1 + import { Router } from "express";
2 + import { requireAuth } from "@app/middleware/auth";
3 + import { InsightsController } from "@app/controllers/insights";
4 +
5 + const router = Router();
6 +
7 + router.get("/", requireAuth, InsightsController.index);
8 +
9 + export default router;
api/src/schemas/index.ts
1 1
export {
2 2
CreateMoodSchema,
3 -
MoodComponentSchema
4 -
} from '@app/schemas/mood.schema';
3 +
MoodComponentSchema,
4 +
} from "@app/schemas/mood.schema";
5 5
6 -
export type {
7 -
CreateMoodInput
8 -
} from '@app/schemas/mood.schema';
6 +
export type { CreateMoodInput } from "@app/schemas/mood.schema";
9 7
10 8
export {
11 9
CreateUserSchema,
12 10
UpdateUserSchema,
13 11
type CreateUserInput,
14 12
type UpdateUserInput,
15 -
} from '@app/schemas/user.schema';
13 +
} from "@app/schemas/user.schema";
16 14
17 15
export {
18 16
LoginSchema,
19 17
PasswordResetRequestSchema,
20 18
PasswordResetSchema,
21 19
ActivateUserSchema,
22 20
type LoginInput,
23 21
type PasswordResetRequestInput,
24 22
type PasswordResetInput,
25 -
type ActivateUserInput
26 -
} from '@app/schemas/auth.schema';
23 +
type ActivateUserInput,
24 +
} from "@app/schemas/auth.schema";
27 25
28 26
export {
29 27
type CreateSleepRecordInput,
30 -
CreateSleepRecordSchema
31 -
} from '@app/schemas/sleepRecord.schema';
28 +
CreateSleepRecordSchema,
29 +
} from "@app/schemas/sleepRecord.schema";
32 30
33 31
export {
34 32
type CreateTriggerInput,
35 -
CreateTriggerSchema
36 -
} from '@app/schemas/trigger.schema';
33 +
CreateTriggerSchema,
34 +
} from "@app/schemas/trigger.schema";
35 +
36 +
export {
37 +
InsightsQuerySchema,
38 +
type InsightsQueryInput,
39 +
} from "@app/schemas/insight.schema";
api/src/schemas/insight.schema.ts
@@ -0,0 +1,10 @@
1 + import { z } from 'zod';
2 + import { InsightPeriod, InsightType } from '@prisma/client';
3 +
4 + export const InsightsQuerySchema = z.object({
5 + type: z.nativeEnum(InsightType).optional(),
6 + period: z.nativeEnum(InsightPeriod).optional(),
7 + limit: z.coerce.number().int().positive().max(100).optional(),
8 + });
9 +
10 + export type InsightsQueryInput = z.infer<typeof InsightsQuerySchema>;
api/src/services/insight.service.ts
@@ -0,0 +1,31 @@
1 + import { prisma } from "@app/lib/prisma";
2 + import { Insight, InsightType, InsightPeriod } from "@prisma/client";
3 +
4 + export const getInsightsByUserId = async (
5 + userId: number,
6 + params?: {
7 + type?: InsightType;
8 + period?: InsightPeriod;
9 + limit?: number;
10 + },
11 + ): Promise<Insight[]> => {
12 + return prisma.insight.findMany({
13 + where: {
14 + userId,
15 + ...(params?.type && { type: params.type }),
16 + ...(params?.period && { period: params.period }),
17 + },
18 + orderBy: { generatedAt: "desc" },
19 + take: params?.limit ?? 10,
20 + });
21 + };
22 +
23 + export const getLatestInsightByType = async (
24 + userId: number,
25 + type: InsightType,
26 + ): Promise<Insight | null> => {
27 + return prisma.insight.findFirst({
28 + where: { userId, type },
29 + orderBy: { generatedAt: "desc" },
30 + });
31 + };