api: link trigger and moods
Parents:
1b7bbac9 file(s) changed
- api/src/controllers/triggerMoodLinks.ts +41 -0
- api/src/controllers/triggers.ts +39 -2
- api/src/createApp.ts +4 -0
- api/src/lib/queue/processors/insights/triggerPattern.ts +2 -0
- api/src/routes/triggers.ts +2 -0
- api/src/schemas/triggerMoodLink.schema.ts +8 -0
- api/src/services/mood.service.ts +8 -2
- api/src/services/trigger.service.ts +9 -3
- api/src/services/triggerMoodLink.service.ts +33 -0
api/src/controllers/triggerMoodLinks.ts
@@ -0,0 +1,41 @@
1 + import { Response, NextFunction } from 'express';
2 + import { AuthenticatedRequest } from '@app/middleware/auth';
3 + import { LinkMoodSchema } from '@app/schemas';
4 + import {
5 + linkTriggerToMood,
6 + unlinkTriggerFromMood,
7 + } from '@app/services/triggerMoodLink.service';
8 +
9 + export const TriggerMoodLinksController = {
10 + linkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
11 + try {
12 + const parsed = LinkMoodSchema.safeParse(req.body);
13 +
14 + if (!parsed.success) {
15 + return res.status(400).json({ errors: parsed.error!.issues });
16 + }
17 +
18 + const triggerId = Number(req.params.triggerId);
19 + const { moodId, perceivedImpact } = parsed.data;
20 +
21 + const link = await linkTriggerToMood(triggerId, moodId, perceivedImpact);
22 +
23 + return res.status(201).json({ link });
24 + } catch (err) {
25 + next(err);
26 + }
27 + },
28 +
29 + unlinkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
30 + try {
31 + const triggerId = Number(req.params.triggerId);
32 + const moodId = Number(req.params.moodId);
33 +
34 + await unlinkTriggerFromMood(triggerId, moodId);
35 +
36 + return res.status(200).json({});
37 + } catch (err) {
38 + next(err);
39 + }
40 + },
41 + };
api/src/controllers/triggers.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import {
4 4
createTrigger,
5 5
getTriggersByUserId,
6 6
destroyTriggerById,
7 7
getTriggerById,
8 8
updateTriggerById
9 9
} from '@app/services/trigger.service';
10 +
import {
11 +
linkTriggerToMood,
12 +
unlinkTriggerFromMood,
13 +
} from '@app/services/triggerMoodLink.service';
10 14
11 15
import {
12 16
CreateTriggerSchema,
13 -
UpdateTriggerSchema
17 +
UpdateTriggerSchema,
18 +
LinkMoodSchema,
14 19
} from '@app/schemas'
15 20
16 21
export const TriggersController = {
17 22
create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
18 23
try {
▸ 64 unchanged lines
83 88
84 89
return res.status(200).json({ trigger });
85 90
} catch (err) {
86 91
next(err);
87 92
}
88 -
}
93 +
},
94 +
95 +
linkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
96 +
try {
97 +
const parsed = LinkMoodSchema.safeParse(req.body);
98 +
99 +
if (!parsed.success) {
100 +
return res.status(400).json({ errors: parsed.error!.issues });
101 +
}
102 +
103 +
const triggerId = Number(req.params.triggerId);
104 +
const { moodId, perceivedImpact } = parsed.data;
105 +
106 +
const link = await linkTriggerToMood(triggerId, moodId, perceivedImpact);
107 +
108 +
return res.status(201).json({ link });
109 +
} catch (err) {
110 +
next(err);
111 +
}
112 +
},
113 +
114 +
unlinkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
115 +
try {
116 +
const triggerId = Number(req.params.triggerId);
117 +
const moodId = Number(req.params.moodId);
118 +
119 +
await unlinkTriggerFromMood(triggerId, moodId);
120 +
121 +
return res.status(200).json({});
122 +
} catch (err) {
123 +
next(err);
124 +
}
125 +
},
89 126
};
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";
▸ 1 unchanged lines
7 7
import authRouter from "@app/routes/auth";
8 8
import moodRouter from "@app/routes/moods";
9 9
import triggerRouter from "@app/routes/triggers";
10 10
import sleepRecordRouter from "@app/routes/sleepRecords";
11 11
import insightRouter from "@app/routes/insights";
12 +
import careActionsRouter from "@app/routes/careActions";
13 +
import medicineRegimensRouter from "@app/routes/medicineRegimens";
12 14
import { errorHandler } from "@app/middleware/errorHandler";
13 15
import { PrismaClient } from "@prisma/client";
14 16
import {
15 17
bootWorkers,
16 18
closeAllWorkers,
▸ 43 unchanged lines
60 62
app.use("/auth", authRouter);
61 63
app.use("/moods", moodRouter);
62 64
app.use("/sleep_records", sleepRecordRouter);
63 65
app.use("/triggers", triggerRouter);
64 66
app.use("/insights", insightRouter);
67 +
app.use("/care-actions", careActionsRouter);
68 +
app.use("/medicine-regimens", medicineRegimensRouter);
65 69
66 70
app.use(errorHandler); // always last
67 71
68 72
// TODO: flesh the actual page here
69 73
app.use(express.static(path.join(__dirname, "./static")));
▸ 9 unchanged lines
79 83
80 84
const shutdown = async () => {
81 85
await Promise.all([closeAllWorkers(), closeAllQueues()]);
82 86
process.exit(0);
83 87
};
api/src/lib/queue/processors/insights/triggerPattern.ts
1 1
import { prisma } from "@app/lib/prisma";
2 2
import { InsightType, InsightPeriod, TriggerType } from "@prisma/client";
3 3
import { InsightPeriodPayload } from "@app/lib/queue/types";
4 4
5 5
const TRIGGER_LABEL: Record<TriggerType, string> = {
6 6
SOCIAL: "social",
7 7
WORK: "trabalho",
8 8
HEALTH: "saúde",
9 9
PHYSICAL: "físico",
10 10
FAMILY: "família",
11 +
THERAPY: "terapia",
12 +
INTERNAL: "interno",
11 13
OTHER: "outros",
12 14
};
13 15
14 16
export async function processTriggerPattern({
15 17
userId,
▸ 76 unchanged lines
92 94
`
93 95
}
94 96
95 97
return template.replace(/\s*\n\s*/g, " ").trim();
96 98
}
api/src/routes/triggers.ts
1 1
import { Router } from "express";
2 2
import { TriggersController } from '@app/controllers/triggers'
3 3
import { requireAuth } from '@app/middleware/auth';
4 4
5 5
const router = Router();
▸ 1 unchanged lines
7 7
router.get('/', requireAuth, TriggersController.index);
8 8
router.get('/:id', requireAuth, TriggersController.show);
9 9
router.post('/', requireAuth, TriggersController.create);
10 10
router.put('/:id', requireAuth, TriggersController.update);
11 11
router.delete('/:id', requireAuth, TriggersController.destroy);
12 +
router.post('/:triggerId/link-mood', requireAuth, TriggersController.linkMood);
13 +
router.delete('/:triggerId/link-mood/:moodId', requireAuth, TriggersController.unlinkMood);
12 14
13 15
export default router;
api/src/schemas/triggerMoodLink.schema.ts
@@ -0,0 +1,8 @@
1 + import { z } from 'zod';
2 +
3 + export const LinkMoodSchema = z.object({
4 + moodId: z.number().int().positive(),
5 + perceivedImpact: z.number().int().min(1).max(5),
6 + });
7 +
8 + export type LinkMoodInput = z.infer<typeof LinkMoodSchema>;
api/src/services/mood.service.ts
1 1
import { prisma } from "@app/lib/prisma";
2 2
import { Mood } from "@prisma/client";
3 3
import { rollingPeriod } from "@app/lib/queue/processors/insights/utils";
4 4
import { InsightJobName } from "@app/lib/queue/types";
5 5
import { getQueue } from "@app/lib/queue";
▸ 71 unchanged lines
77 77
page,
78 78
nextPage: page < totalPages ? page + 1 : null,
79 79
};
80 80
};
81 81
82 -
export const getMoodById = async (id: number): Promise<Mood | null> => {
82 +
export const getMoodById = async (id: number) => {
83 83
return await prisma.mood.findUnique({
84 84
where: {
85 85
id,
86 86
},
87 -
include: { moodComponents: true },
87 +
include: {
88 +
moodComponents: true,
89 +
triggerLinks: {
90 +
include: { trigger: true },
91 +
orderBy: { linkedAt: 'desc' },
92 +
},
93 +
},
88 94
});
89 95
};
90 96
91 97
export const destroyMoodById = async (
92 98
userId: number,
▸ 42 unchanged lines
135 141
removeOnFail: true,
136 142
},
137 143
},
138 144
]);
139 145
};
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";
▸ 71 unchanged lines
77 77
void enqueueTriggerPatternInsight(userId);
78 78
79 79
return trigger
80 80
}
81 81
82 -
export const getTriggerById = async (userId: number, id: number): Promise<Trigger | null> => {
82 +
export const getTriggerById = async (userId: number, id: number) => {
83 83
return await prisma.trigger.findUnique({
84 -
where: { userId, id }
85 -
})
84 +
where: { userId, id },
85 +
include: {
86 +
moodLinks: {
87 +
include: { mood: { include: { moodComponents: true } } },
88 +
orderBy: { linkedAt: 'desc' },
89 +
},
90 +
},
91 +
});
86 92
}
87 93
88 94
export const updateTriggerById = async (
89 95
userId: number, id: number, input: UpdateTriggerInput
90 96
): Promise<Trigger> => {
▸ 19 unchanged lines
110 116
removeOnComplete: true,
111 117
removeOnFail: true,
112 118
}
113 119
);
114 120
}
api/src/services/triggerMoodLink.service.ts
@@ -0,0 +1,33 @@
1 + import { prisma } from '@app/lib/prisma';
2 + import { TriggerMoodLink } from '@prisma/client';
3 +
4 + export const linkTriggerToMood = async (
5 + triggerId: number,
6 + moodId: number,
7 + perceivedImpact: number,
8 + ): Promise<TriggerMoodLink> => {
9 + const existing = await prisma.triggerMoodLink.findUnique({
10 + where: {
11 + triggerId_moodId: { triggerId, moodId },
12 + },
13 + });
14 +
15 + if (existing) {
16 + throw new Error(`Link between trigger ${triggerId} and mood ${moodId} already exists`);
17 + }
18 +
19 + return prisma.triggerMoodLink.create({
20 + data: { triggerId, moodId, perceivedImpact },
21 + });
22 + };
23 +
24 + export const unlinkTriggerFromMood = async (
25 + triggerId: number,
26 + moodId: number,
27 + ): Promise<TriggerMoodLink> => {
28 + return prisma.triggerMoodLink.delete({
29 + where: {
30 + triggerId_moodId: { triggerId, moodId },
31 + },
32 + });
33 + };