@molecule/api-resource-medication 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +402 -0
- package/package.json +12 -6
package/README.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T01:49:15.190Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-resource-medication
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
`@molecule/api-resource-medication` — medication tracking with
|
|
16
|
+
dosing schedule + adherence log + adherence-rate calc.
|
|
17
|
+
|
|
18
|
+
Owner-scoped: every medication and dose log belongs to the authenticated
|
|
19
|
+
user. Medications carry dosage, `frequency` (e.g. `daily`, `twice_daily`),
|
|
20
|
+
`times_of_day`, and active date range; dose logs record
|
|
21
|
+
taken/skipped/late/missed and `adherenceRate` aggregates them over a date
|
|
22
|
+
range.
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createMedicationRouter } from '@molecule/api-resource-medication'
|
|
28
|
+
app.use('/medications', createMedicationRouter())
|
|
29
|
+
// GET / · POST / · GET /adherence?from=…&to=… · GET|PUT|DELETE /:id
|
|
30
|
+
// GET /:id/logs · POST /:id/logs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { adherenceRate, createMedicationForOwner, logDose } from '@molecule/api-resource-medication'
|
|
35
|
+
|
|
36
|
+
const med = await createMedicationForOwner(userId, {
|
|
37
|
+
name: 'Metformin',
|
|
38
|
+
dosage: '500 mg',
|
|
39
|
+
frequency: 'twice_daily',
|
|
40
|
+
})
|
|
41
|
+
await logDose(med.id, userId, { status: 'taken' })
|
|
42
|
+
const { rate } = await adherenceRate(userId, '2026-01-01', '2026-01-31')
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Type
|
|
46
|
+
|
|
47
|
+
`resource`
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @molecule/api-resource-medication @molecule/api-bonds-default-express @molecule/api-database @molecule/api-i18n @molecule/api-middleware-validation express zod
|
|
53
|
+
npm install -D @types/express
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### Interfaces
|
|
59
|
+
|
|
60
|
+
#### `MedicationLogRow`
|
|
61
|
+
|
|
62
|
+
Database row shape for a medication intake log entry.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface MedicationLogRow {
|
|
66
|
+
id: string
|
|
67
|
+
medication_id: string
|
|
68
|
+
owner_id: string
|
|
69
|
+
taken_at: string | Date
|
|
70
|
+
status: 'taken' | 'skipped' | 'late' | 'missed'
|
|
71
|
+
notes: string | null
|
|
72
|
+
created_at: string | Date
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### `MedicationRow`
|
|
77
|
+
|
|
78
|
+
Database row shape for a medication record.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
interface MedicationRow {
|
|
82
|
+
id: string
|
|
83
|
+
owner_id: string
|
|
84
|
+
name: string
|
|
85
|
+
generic_name: string | null
|
|
86
|
+
dosage: string
|
|
87
|
+
unit: string | null
|
|
88
|
+
frequency: MedicationFrequency
|
|
89
|
+
times_of_day: string[]
|
|
90
|
+
start_date: string | Date | null
|
|
91
|
+
end_date: string | Date | null
|
|
92
|
+
notes: string | null
|
|
93
|
+
is_active: boolean
|
|
94
|
+
created_at: string | Date
|
|
95
|
+
updated_at: string | Date
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Types
|
|
100
|
+
|
|
101
|
+
#### `MedicationFrequency`
|
|
102
|
+
|
|
103
|
+
Describes how often a medication is taken (e.g. daily, twice daily, as needed).
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
type MedicationFrequency =
|
|
107
|
+
| 'once'
|
|
108
|
+
| 'daily'
|
|
109
|
+
| 'twice_daily'
|
|
110
|
+
| 'three_times_daily'
|
|
111
|
+
| 'four_times_daily'
|
|
112
|
+
| 'as_needed'
|
|
113
|
+
| 'weekly'
|
|
114
|
+
| 'custom'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Functions
|
|
118
|
+
|
|
119
|
+
#### `adherenceRate(ownerId, from, to)`
|
|
120
|
+
|
|
121
|
+
Adherence summary: percentage of `taken` logs out of all logs in window.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
function adherenceRate(
|
|
125
|
+
ownerId: string,
|
|
126
|
+
from: string,
|
|
127
|
+
to: string,
|
|
128
|
+
): Promise<{ taken: number; total: number; rate: number }>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### `createMedicationForOwner(ownerId, data)`
|
|
132
|
+
|
|
133
|
+
Creates a new medication record owned by the given owner and returns the persisted row.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
function createMedicationForOwner(
|
|
137
|
+
ownerId: string,
|
|
138
|
+
data: {
|
|
139
|
+
name: string
|
|
140
|
+
generic_name?: string | null
|
|
141
|
+
dosage: string
|
|
142
|
+
unit?: string | null
|
|
143
|
+
frequency?: MedicationFrequency
|
|
144
|
+
times_of_day?: string[]
|
|
145
|
+
start_date?: string | null
|
|
146
|
+
end_date?: string | null
|
|
147
|
+
notes?: string | null
|
|
148
|
+
},
|
|
149
|
+
): Promise<MedicationRow>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `createMedicationRouter()`
|
|
153
|
+
|
|
154
|
+
Creates and returns the Express router for medication CRUD and dose-logging endpoints.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
function createMedicationRouter(): Router
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `deleteMedicationForOwner(medicationId, ownerId)`
|
|
161
|
+
|
|
162
|
+
Deletes a medication by ID, returning true on success or false if not found or not owned.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
function deleteMedicationForOwner(medicationId: string, ownerId: string): Promise<boolean>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### `getMedicationForOwner(medicationId, ownerId)`
|
|
169
|
+
|
|
170
|
+
Fetches a single medication by ID, returning null if not found or not owned by the given owner.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
function getMedicationForOwner(medicationId: string, ownerId: string): Promise<MedicationRow | null>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### `listLogs(medicationId, ownerId, opts?)`
|
|
177
|
+
|
|
178
|
+
Returns dose logs for a medication within an optional time window, or null if the medication is not found or not owned.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
function listLogs(
|
|
182
|
+
medicationId: string,
|
|
183
|
+
ownerId: string,
|
|
184
|
+
opts?: { from?: string; to?: string; limit?: number },
|
|
185
|
+
): Promise<MedicationLogRow[] | null>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### `listMedicationsForOwner(ownerId, opts?)`
|
|
189
|
+
|
|
190
|
+
Returns all medications belonging to the given owner, optionally including inactive ones.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
function listMedicationsForOwner(
|
|
194
|
+
ownerId: string,
|
|
195
|
+
opts?: { include_inactive?: boolean },
|
|
196
|
+
): Promise<MedicationRow[]>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `logDose(medicationId, ownerId, data)`
|
|
200
|
+
|
|
201
|
+
Log a dose. If `status` not provided, infers from `taken_at` vs scheduled times.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
function logDose(
|
|
205
|
+
medicationId: string,
|
|
206
|
+
ownerId: string,
|
|
207
|
+
data: {
|
|
208
|
+
taken_at?: string
|
|
209
|
+
status?: 'taken' | 'skipped' | 'late' | 'missed'
|
|
210
|
+
notes?: string | null
|
|
211
|
+
},
|
|
212
|
+
): Promise<MedicationLogRow | null>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### `updateMedicationForOwner(medicationId, ownerId, patch)`
|
|
216
|
+
|
|
217
|
+
Applies a partial patch to a medication, returning the updated row or null if not found or not owned.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
function updateMedicationForOwner(
|
|
221
|
+
medicationId: string,
|
|
222
|
+
ownerId: string,
|
|
223
|
+
patch: Partial<MedicationRow>,
|
|
224
|
+
): Promise<MedicationRow | null>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Constants
|
|
228
|
+
|
|
229
|
+
#### `LOG_STATUSES`
|
|
230
|
+
|
|
231
|
+
Valid status values for a medication dose log entry.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const LOG_STATUSES: readonly ['taken', 'skipped', 'late', 'missed']
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### `logCreateSchema`
|
|
238
|
+
|
|
239
|
+
Zod schema for validating medication dose log creation request payloads.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
const logCreateSchema: z.ZodObject<
|
|
243
|
+
{
|
|
244
|
+
taken_at: z.ZodOptional<z.ZodString>
|
|
245
|
+
status: z.ZodOptional<
|
|
246
|
+
z.ZodEnum<{ taken: 'taken'; skipped: 'skipped'; late: 'late'; missed: 'missed' }>
|
|
247
|
+
>
|
|
248
|
+
notes: z.ZodOptional<z.ZodNullable<z.ZodString>>
|
|
249
|
+
},
|
|
250
|
+
z.core.$strip
|
|
251
|
+
>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### `MEDICATION_FREQUENCIES`
|
|
255
|
+
|
|
256
|
+
Valid dosing frequency options for a medication schedule.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const MEDICATION_FREQUENCIES: readonly [
|
|
260
|
+
'once',
|
|
261
|
+
'daily',
|
|
262
|
+
'twice_daily',
|
|
263
|
+
'three_times_daily',
|
|
264
|
+
'four_times_daily',
|
|
265
|
+
'as_needed',
|
|
266
|
+
'weekly',
|
|
267
|
+
'custom',
|
|
268
|
+
]
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### `medicationCreateSchema`
|
|
272
|
+
|
|
273
|
+
Zod schema for validating medication creation request payloads.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const medicationCreateSchema: z.ZodObject<
|
|
277
|
+
{
|
|
278
|
+
name: z.ZodString
|
|
279
|
+
generic_name: z.ZodOptional<z.ZodNullable<z.ZodString>>
|
|
280
|
+
dosage: z.ZodString
|
|
281
|
+
unit: z.ZodOptional<z.ZodNullable<z.ZodString>>
|
|
282
|
+
frequency: z.ZodOptional<
|
|
283
|
+
z.ZodEnum<{
|
|
284
|
+
once: 'once'
|
|
285
|
+
daily: 'daily'
|
|
286
|
+
twice_daily: 'twice_daily'
|
|
287
|
+
three_times_daily: 'three_times_daily'
|
|
288
|
+
four_times_daily: 'four_times_daily'
|
|
289
|
+
as_needed: 'as_needed'
|
|
290
|
+
weekly: 'weekly'
|
|
291
|
+
custom: 'custom'
|
|
292
|
+
}>
|
|
293
|
+
>
|
|
294
|
+
times_of_day: z.ZodOptional<z.ZodArray<z.ZodString>>
|
|
295
|
+
start_date: z.ZodOptional<z.ZodNullable<z.ZodString>>
|
|
296
|
+
end_date: z.ZodOptional<z.ZodNullable<z.ZodString>>
|
|
297
|
+
notes: z.ZodOptional<z.ZodNullable<z.ZodString>>
|
|
298
|
+
},
|
|
299
|
+
z.core.$strip
|
|
300
|
+
>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### `medicationUpdateSchema`
|
|
304
|
+
|
|
305
|
+
Zod schema for validating medication update request payloads; all create fields become optional and `is_active` is added.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
const medicationUpdateSchema: z.ZodObject<
|
|
309
|
+
{
|
|
310
|
+
name: z.ZodOptional<z.ZodString>
|
|
311
|
+
generic_name: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
|
|
312
|
+
dosage: z.ZodOptional<z.ZodString>
|
|
313
|
+
unit: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
|
|
314
|
+
frequency: z.ZodOptional<
|
|
315
|
+
z.ZodOptional<
|
|
316
|
+
z.ZodEnum<{
|
|
317
|
+
once: 'once'
|
|
318
|
+
daily: 'daily'
|
|
319
|
+
twice_daily: 'twice_daily'
|
|
320
|
+
three_times_daily: 'three_times_daily'
|
|
321
|
+
four_times_daily: 'four_times_daily'
|
|
322
|
+
as_needed: 'as_needed'
|
|
323
|
+
weekly: 'weekly'
|
|
324
|
+
custom: 'custom'
|
|
325
|
+
}>
|
|
326
|
+
>
|
|
327
|
+
>
|
|
328
|
+
times_of_day: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>
|
|
329
|
+
start_date: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
|
|
330
|
+
end_date: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
|
|
331
|
+
notes: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
|
|
332
|
+
is_active: z.ZodOptional<z.ZodBoolean>
|
|
333
|
+
},
|
|
334
|
+
z.core.$strip
|
|
335
|
+
>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Injection Notes
|
|
339
|
+
|
|
340
|
+
### Requirements
|
|
341
|
+
|
|
342
|
+
Peer dependencies:
|
|
343
|
+
|
|
344
|
+
- `@molecule/api-bonds-default-express` ^1.0.1
|
|
345
|
+
- `@molecule/api-database` ^1.0.1
|
|
346
|
+
- `@molecule/api-i18n` ^1.0.1
|
|
347
|
+
- `@molecule/api-middleware-validation` ^1.0.1
|
|
348
|
+
- `express` ^5.0.0
|
|
349
|
+
- `zod` ^4.0.0
|
|
350
|
+
|
|
351
|
+
### Runtime Dependencies
|
|
352
|
+
|
|
353
|
+
- `@molecule/api-bonds-default-express`
|
|
354
|
+
- `@molecule/api-database`
|
|
355
|
+
- `@molecule/api-i18n`
|
|
356
|
+
- `@molecule/api-middleware-validation`
|
|
357
|
+
- `express`
|
|
358
|
+
- `zod`
|
|
359
|
+
|
|
360
|
+
Tables: `src/__setup__/medications.sql` creates `medications` +
|
|
361
|
+
`medication_logs`. An mlcl-scaffolded API replays `__setup__/*.sql`
|
|
362
|
+
automatically on migrate; anywhere else run it once — nothing at runtime
|
|
363
|
+
creates them.
|
|
364
|
+
|
|
365
|
+
The router does not authenticate — it reads the caller from
|
|
366
|
+
`res.locals.session` (populated by your global auth middleware) and 401s
|
|
367
|
+
without a session. All service functions are `…ForOwner(…, ownerId)` and
|
|
368
|
+
return `null` for rows the caller doesn't own — always pass the
|
|
369
|
+
AUTHENTICATED user's id, never a client-sent one.
|
|
370
|
+
|
|
371
|
+
## E2E Tests
|
|
372
|
+
|
|
373
|
+
Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
374
|
+
each item to this app's actual screens/flows, and check every box off one by
|
|
375
|
+
one. This is HEALTH data: a box you can't check is a correctness or privacy
|
|
376
|
+
bug to fix, never a skip:
|
|
377
|
+
|
|
378
|
+
- [ ] Adding a medication persists its real fields — `name`, `dosage`,
|
|
379
|
+
`frequency`, `times_of_day` — and the med then appears in the user's list
|
|
380
|
+
(`GET /`) carrying those exact values, not a truncated or defaulted copy.
|
|
381
|
+
- [ ] The schedule matches the frequency: a `twice_daily` med carries two
|
|
382
|
+
`times_of_day` entries and the UI shows two dose slots for the day — not one
|
|
383
|
+
and not three; a `daily` med shows exactly one. (Nothing derives the times
|
|
384
|
+
from `frequency` automatically, so a mismatch is a real bug to catch here.)
|
|
385
|
+
- [ ] Logging a dose records it against the right medication and time:
|
|
386
|
+
`POST /:id/logs` with `status: 'taken'` (and its `taken_at`) then shows in
|
|
387
|
+
`GET /:id/logs`, and the adherence figure (`GET /adherence`) moves.
|
|
388
|
+
- [ ] A skipped or missed dose is reflected HONESTLY — logged with
|
|
389
|
+
`status: 'missed'`/`'skipped'` it counts toward `total` but NOT `taken`, so
|
|
390
|
+
it lowers the adherence rate; it is never silently counted as taken (the
|
|
391
|
+
default status is `taken`, so a miss must be logged as a miss, not omitted).
|
|
392
|
+
- [ ] CORRECTNESS — adherence is a true ratio of logged doses: a brand-new
|
|
393
|
+
med with zero logged doses reads 0% (`rate` 0, `total` 0), never 100%; a
|
|
394
|
+
partial day (some doses logged, some not yet) is never shown as complete.
|
|
395
|
+
- [ ] If the app tracks refills/supply on top of this resource (the core does
|
|
396
|
+
not model one), recording a fill decrements the refills-remaining count and
|
|
397
|
+
a low-supply threshold raises a visible refill flag — verify both live.
|
|
398
|
+
- [ ] PRIVACY/AUTHORIZATION — medication data is strictly per-user: signed in
|
|
399
|
+
as user B, guessing user A's medication id on `GET /:id`, `PUT /:id`,
|
|
400
|
+
`DELETE /:id`, or `/:id/logs` returns 404 (owner-scoped), never A's row; an
|
|
401
|
+
unauthenticated request 401s. Confirm PHI (name, dosage, notes) is never
|
|
402
|
+
written to server logs in the clear.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-resource-medication",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Medication tracking + dosing schedule + adherence log",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -27,6 +28,11 @@
|
|
|
27
28
|
"resource"
|
|
28
29
|
],
|
|
29
30
|
"license": "Apache-2.0",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
34
|
+
"directory": "packages/api/resources/medication"
|
|
35
|
+
},
|
|
30
36
|
"devDependencies": {
|
|
31
37
|
"@types/express": "5.0.6",
|
|
32
38
|
"@types/node": "26.1.2",
|
|
@@ -36,10 +42,10 @@
|
|
|
36
42
|
"zod": "4.4.3"
|
|
37
43
|
},
|
|
38
44
|
"peerDependencies": {
|
|
39
|
-
"@molecule/api-bonds-default-express": "^1.0.
|
|
40
|
-
"@molecule/api-database": "^1.0.
|
|
41
|
-
"@molecule/api-i18n": "^1.0.
|
|
42
|
-
"@molecule/api-middleware-validation": "^1.0.
|
|
45
|
+
"@molecule/api-bonds-default-express": "^1.0.1",
|
|
46
|
+
"@molecule/api-database": "^1.0.1",
|
|
47
|
+
"@molecule/api-i18n": "^1.0.1",
|
|
48
|
+
"@molecule/api-middleware-validation": "^1.0.1",
|
|
43
49
|
"express": "^5.0.0",
|
|
44
50
|
"zod": "^4.0.0"
|
|
45
51
|
}
|