@molecule/api-resource-journal-entry 1.0.0 → 1.0.2
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 +422 -0
- package/package.json +16 -9
package/README.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
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:14.793Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-resource-journal-entry
|
|
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-journal-entry` — owner-scoped diary entries
|
|
16
|
+
with optional at-rest encryption, mood-score linkage, daily-write
|
|
17
|
+
streaks, and JSON/CSV/TXT export.
|
|
18
|
+
|
|
19
|
+
Extracted from the mental-health-journal flagship. When
|
|
20
|
+
`@molecule/api-encryption` is bonded, entry bodies are stored
|
|
21
|
+
encrypted-at-rest; otherwise the plaintext column is used (the read
|
|
22
|
+
path falls back automatically so key rotation never bricks reads).
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createJournalEntryRouter } from '@molecule/api-resource-journal-entry'
|
|
28
|
+
import express from 'express'
|
|
29
|
+
|
|
30
|
+
const app = express()
|
|
31
|
+
app.use('/api/journal', createJournalEntryRouter())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Type
|
|
35
|
+
|
|
36
|
+
`resource`
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @molecule/api-resource-journal-entry @molecule/api-bonds-default-express @molecule/api-database @molecule/api-encryption @molecule/api-i18n @molecule/api-logger @molecule/api-middleware-validation express zod
|
|
42
|
+
npm install -D @types/express
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### Interfaces
|
|
48
|
+
|
|
49
|
+
#### `CreateEntryInput`
|
|
50
|
+
|
|
51
|
+
Input fields accepted when creating a new journal entry.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface CreateEntryInput {
|
|
55
|
+
mood?: MoodLevel
|
|
56
|
+
title?: string
|
|
57
|
+
body: string
|
|
58
|
+
tags?: string[]
|
|
59
|
+
prompt_id?: string | null
|
|
60
|
+
written_at?: string | Date
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### `ExportRecord`
|
|
65
|
+
|
|
66
|
+
Flattened, export-friendly representation of a single journal entry.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
interface ExportRecord {
|
|
70
|
+
id: string
|
|
71
|
+
date: string
|
|
72
|
+
title: string
|
|
73
|
+
mood: MoodLevel
|
|
74
|
+
tags: string[]
|
|
75
|
+
word_count: number
|
|
76
|
+
body: string
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### `JournalEntryRow`
|
|
81
|
+
|
|
82
|
+
Row shape persisted in `journal_entries`.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface JournalEntryRow {
|
|
86
|
+
id: string
|
|
87
|
+
user_id: string
|
|
88
|
+
written_at: string | Date
|
|
89
|
+
title: string | null
|
|
90
|
+
body: string | null
|
|
91
|
+
body_encrypted: string | null
|
|
92
|
+
body_iv: string | null
|
|
93
|
+
mood_id: string | null
|
|
94
|
+
prompt_id: string | null
|
|
95
|
+
activity_ids: unknown
|
|
96
|
+
tags: unknown
|
|
97
|
+
word_count: number
|
|
98
|
+
ai_summary: string | null
|
|
99
|
+
ai_themes: unknown
|
|
100
|
+
is_private: boolean
|
|
101
|
+
created_at: string | Date
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### `MoodEntryRow`
|
|
106
|
+
|
|
107
|
+
Row shape persisted in `mood_entries`.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface MoodEntryRow {
|
|
111
|
+
id: string
|
|
112
|
+
user_id: string
|
|
113
|
+
recorded_at: string | Date
|
|
114
|
+
score: number
|
|
115
|
+
energy: number | null
|
|
116
|
+
anxiety: number | null
|
|
117
|
+
label: string | null
|
|
118
|
+
activities: unknown
|
|
119
|
+
notes: string | null
|
|
120
|
+
journal_entry_id: string | null
|
|
121
|
+
created_at: string | Date
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `PublicJournalEntry`
|
|
126
|
+
|
|
127
|
+
Public-facing decrypted entry (returned by the service / routes).
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
interface PublicJournalEntry {
|
|
131
|
+
id: string
|
|
132
|
+
date: string
|
|
133
|
+
title: string
|
|
134
|
+
preview: string
|
|
135
|
+
body: string
|
|
136
|
+
mood: MoodLevel
|
|
137
|
+
tags: string[]
|
|
138
|
+
word_count: number
|
|
139
|
+
ai_summary: string | null
|
|
140
|
+
ai_themes: string[]
|
|
141
|
+
prompt: string | null
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `UpdateEntryInput`
|
|
146
|
+
|
|
147
|
+
Fields that can be patched on an existing journal entry.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
interface UpdateEntryInput {
|
|
151
|
+
mood?: MoodLevel
|
|
152
|
+
title?: string
|
|
153
|
+
body?: string
|
|
154
|
+
tags?: string[]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Types
|
|
159
|
+
|
|
160
|
+
#### `MoodLevel`
|
|
161
|
+
|
|
162
|
+
Discrete mood-level labels mapped to a 1..5 score.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
type MoodLevel = 'radiant' | 'good' | 'neutral' | 'low' | 'struggling'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Functions
|
|
169
|
+
|
|
170
|
+
#### `computeStreak(userId)`
|
|
171
|
+
|
|
172
|
+
Daily-write streak: consecutive distinct UTC days ending today/yesterday.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
function computeStreak(userId: string): Promise<number>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `createEntryForOwner(userId, input)`
|
|
179
|
+
|
|
180
|
+
Create a journal entry and, if `mood` is provided, upsert today's
|
|
181
|
+
`mood_entries` row + link it to the journal entry.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
function createEntryForOwner(
|
|
185
|
+
userId: string,
|
|
186
|
+
input: CreateEntryInput,
|
|
187
|
+
): Promise<PublicJournalEntry | null>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `createJournalEntryRouter()`
|
|
191
|
+
|
|
192
|
+
Build the journal-entry router.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
function createJournalEntryRouter(): Router
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### `decryptIfNeeded(row)`
|
|
199
|
+
|
|
200
|
+
Decrypt the entry body if encryption is bonded; otherwise return plaintext.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
function decryptIfNeeded(row: JournalEntryRow): Promise<string>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `deleteEntryForOwner(userId, id)`
|
|
207
|
+
|
|
208
|
+
Owner-scoped delete — true if it deleted, false if not owned / missing.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
function deleteEntryForOwner(userId: string, id: string): Promise<boolean>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### `exportEntries(userId, max?)`
|
|
215
|
+
|
|
216
|
+
Serialize all entries for an owner into an export-friendly array.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
function exportEntries(userId: string, max?: number): Promise<ExportRecord[]>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### `formatExport(records, format)`
|
|
223
|
+
|
|
224
|
+
Format export records as JSON / CSV / TXT.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
function formatExport(
|
|
228
|
+
records: ExportRecord[],
|
|
229
|
+
format: 'json' | 'csv' | 'txt',
|
|
230
|
+
): { contentType: string; body: string }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### `getEntryForOwner(userId, id)`
|
|
234
|
+
|
|
235
|
+
Owner-scoped read — returns null when missing or not owned.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
function getEntryForOwner(userId: string, id: string): Promise<PublicJournalEntry | null>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `levelByScore(score)`
|
|
242
|
+
|
|
243
|
+
Round + clamp a raw score and return its discrete label.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
function levelByScore(score: number | null | undefined): MoodLevel
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### `listEntriesForOwner(userId, limit?)`
|
|
250
|
+
|
|
251
|
+
List the most-recent entries for an owner.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
function listEntriesForOwner(userId: string, limit?: number): Promise<PublicJournalEntry[]>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### `normalizeScore(score)`
|
|
258
|
+
|
|
259
|
+
Normalize a 1..5 score to 0..1 (useful for sparkline charts).
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
function normalizeScore(score: number | null | undefined): number
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `shapeEntry(entry, body)`
|
|
266
|
+
|
|
267
|
+
Shape a row into the public-facing representation.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
function shapeEntry(entry: JournalEntryRow, body: string): Promise<PublicJournalEntry>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### `updateEntryForOwner(userId, id, input)`
|
|
274
|
+
|
|
275
|
+
Owner-scoped patch update — returns null when missing or not owned.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
function updateEntryForOwner(
|
|
279
|
+
userId: string,
|
|
280
|
+
id: string,
|
|
281
|
+
input: UpdateEntryInput,
|
|
282
|
+
): Promise<PublicJournalEntry | null>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Constants
|
|
286
|
+
|
|
287
|
+
#### `createEntrySchema`
|
|
288
|
+
|
|
289
|
+
Validator for creating a new journal entry.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const createEntrySchema: z.ZodObject<
|
|
293
|
+
{
|
|
294
|
+
mood: z.ZodOptional<
|
|
295
|
+
z.ZodEnum<{
|
|
296
|
+
radiant: 'radiant'
|
|
297
|
+
good: 'good'
|
|
298
|
+
neutral: 'neutral'
|
|
299
|
+
low: 'low'
|
|
300
|
+
struggling: 'struggling'
|
|
301
|
+
}>
|
|
302
|
+
>
|
|
303
|
+
title: z.ZodOptional<z.ZodString>
|
|
304
|
+
body: z.ZodString
|
|
305
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>
|
|
306
|
+
prompt_id: z.ZodOptional<z.ZodString>
|
|
307
|
+
written_at: z.ZodOptional<z.ZodString>
|
|
308
|
+
},
|
|
309
|
+
z.core.$strip
|
|
310
|
+
>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### `moodLevelSchema`
|
|
314
|
+
|
|
315
|
+
Mood level enum used by journal-entry payloads.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
const moodLevelSchema: z.ZodEnum<{
|
|
319
|
+
radiant: 'radiant'
|
|
320
|
+
good: 'good'
|
|
321
|
+
neutral: 'neutral'
|
|
322
|
+
low: 'low'
|
|
323
|
+
struggling: 'struggling'
|
|
324
|
+
}>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### `SCORE_BY_LEVEL`
|
|
328
|
+
|
|
329
|
+
Maps each MoodLevel label to its corresponding numeric score (1..5).
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const SCORE_BY_LEVEL: Record<MoodLevel, number>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### `updateEntrySchema`
|
|
336
|
+
|
|
337
|
+
Validator for updating an existing journal entry.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const updateEntrySchema: z.ZodObject<
|
|
341
|
+
{
|
|
342
|
+
mood: z.ZodOptional<
|
|
343
|
+
z.ZodOptional<
|
|
344
|
+
z.ZodEnum<{
|
|
345
|
+
radiant: 'radiant'
|
|
346
|
+
good: 'good'
|
|
347
|
+
neutral: 'neutral'
|
|
348
|
+
low: 'low'
|
|
349
|
+
struggling: 'struggling'
|
|
350
|
+
}>
|
|
351
|
+
>
|
|
352
|
+
>
|
|
353
|
+
title: z.ZodOptional<z.ZodOptional<z.ZodString>>
|
|
354
|
+
tags: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>
|
|
355
|
+
prompt_id: z.ZodOptional<z.ZodOptional<z.ZodString>>
|
|
356
|
+
written_at: z.ZodOptional<z.ZodOptional<z.ZodString>>
|
|
357
|
+
body: z.ZodOptional<z.ZodString>
|
|
358
|
+
},
|
|
359
|
+
z.core.$strip
|
|
360
|
+
>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Injection Notes
|
|
364
|
+
|
|
365
|
+
### Requirements
|
|
366
|
+
|
|
367
|
+
Peer dependencies:
|
|
368
|
+
|
|
369
|
+
- `@molecule/api-bonds-default-express` ^1.0.1
|
|
370
|
+
- `@molecule/api-logger` ^1.0.1
|
|
371
|
+
- `@molecule/api-database` ^1.0.1
|
|
372
|
+
- `@molecule/api-encryption` ^1.0.1
|
|
373
|
+
- `@molecule/api-i18n` ^1.0.1
|
|
374
|
+
- `@molecule/api-middleware-validation` ^1.0.1
|
|
375
|
+
- `express` ^5.0.0
|
|
376
|
+
- `zod` ^4.0.0
|
|
377
|
+
|
|
378
|
+
### Runtime Dependencies
|
|
379
|
+
|
|
380
|
+
- `@molecule/api-bonds-default-express`
|
|
381
|
+
- `@molecule/api-database`
|
|
382
|
+
- `@molecule/api-encryption`
|
|
383
|
+
- `@molecule/api-i18n`
|
|
384
|
+
- `@molecule/api-logger`
|
|
385
|
+
- `@molecule/api-middleware-validation`
|
|
386
|
+
- `express`
|
|
387
|
+
- `zod`
|
|
388
|
+
|
|
389
|
+
Schema lives in `__setup__/journal_entries.sql` — two tables:
|
|
390
|
+
`journal_entries` + `mood_entries`. Mood rows are upserted per
|
|
391
|
+
(user, day) so multiple entries in a day share one mood row.
|
|
392
|
+
|
|
393
|
+
## E2E Tests
|
|
394
|
+
|
|
395
|
+
Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
396
|
+
each item to this app's actual journal screens/flows, and check every box
|
|
397
|
+
off one by one. A box you can't check is an integration bug to fix — not a
|
|
398
|
+
skip. This is a PRIVATE journal, so privacy is the defining requirement:
|
|
399
|
+
|
|
400
|
+
- [ ] Writing an entry persists its real fields — title, body, mood
|
|
401
|
+
(radiant/good/neutral/low/struggling), tags, and date (written_at) — and
|
|
402
|
+
the entry then appears in the author's journal list dated correctly (the
|
|
403
|
+
list is newest-first by written_at) with a word_count matching the body.
|
|
404
|
+
- [ ] Editing an entry (title / body / mood / tags) and deleting one both
|
|
405
|
+
reflect in the UI immediately AND survive a reload — the change persisted
|
|
406
|
+
server-side, not just local state.
|
|
407
|
+
- [ ] If the UI filters or searches the list (by tag, mood, or date), only
|
|
408
|
+
matching entries come back, and the mood shown per entry matches the mood
|
|
409
|
+
saved for that day (mood rows upsert once per user per day).
|
|
410
|
+
- [ ] If a write streak or count is shown, it equals the number of
|
|
411
|
+
consecutive distinct days the author actually wrote, counted on the UTC
|
|
412
|
+
day boundary and ending today or yesterday — add or remove entries across
|
|
413
|
+
a day boundary and confirm it recomputes; a JSON/CSV/TXT export downloads
|
|
414
|
+
only the author's own entries.
|
|
415
|
+
- [ ] PRIVACY / AUTHORIZATION — entries are visible ONLY to their author.
|
|
416
|
+
There is no sharing and no cross-user read path: signed in as a second
|
|
417
|
+
user, requesting another user's entry id returns 404 (not-owned and truly
|
|
418
|
+
missing are indistinguishable, so existence is never leaked), and the list
|
|
419
|
+
and export return only your own rows — no admin or global view surfaces
|
|
420
|
+
another user's entries. The owner is always the session user, never a
|
|
421
|
+
userId taken from the request body, and journal content is never written
|
|
422
|
+
to logs in the clear.
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-resource-journal-entry",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Owner-scoped journal entry resource with optional at-rest encryption, mood linkage, daily streaks, and JSON/CSV/TXT export. Extracted from mental-health-journal flagship.",
|
|
5
|
+
"homepage": "https://www.molecule.dev/packages/api-resource-journal-entry",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
@@ -17,7 +18,8 @@
|
|
|
17
18
|
}
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
21
23
|
],
|
|
22
24
|
"keywords": [
|
|
23
25
|
"molecule",
|
|
@@ -28,21 +30,26 @@
|
|
|
28
30
|
"resource"
|
|
29
31
|
],
|
|
30
32
|
"license": "Apache-2.0",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
36
|
+
"directory": "packages/api/resources/journal-entry"
|
|
37
|
+
},
|
|
31
38
|
"devDependencies": {
|
|
32
39
|
"@types/express": "5.0.6",
|
|
33
40
|
"@types/node": "26.1.2",
|
|
34
41
|
"express": "5.2.1",
|
|
35
42
|
"typescript": "6.0.3",
|
|
36
|
-
"vitest": "4.1.
|
|
43
|
+
"vitest": "4.1.11",
|
|
37
44
|
"zod": "4.4.3"
|
|
38
45
|
},
|
|
39
46
|
"peerDependencies": {
|
|
40
|
-
"@molecule/api-bonds-default-express": "^1.0.
|
|
41
|
-
"@molecule/api-logger": "^1.0.
|
|
42
|
-
"@molecule/api-database": "^1.0.
|
|
43
|
-
"@molecule/api-encryption": "^1.0.
|
|
44
|
-
"@molecule/api-i18n": "^1.0.
|
|
45
|
-
"@molecule/api-middleware-validation": "^1.0.
|
|
47
|
+
"@molecule/api-bonds-default-express": "^1.0.1",
|
|
48
|
+
"@molecule/api-logger": "^1.0.1",
|
|
49
|
+
"@molecule/api-database": "^1.0.1",
|
|
50
|
+
"@molecule/api-encryption": "^1.0.1",
|
|
51
|
+
"@molecule/api-i18n": "^1.0.1",
|
|
52
|
+
"@molecule/api-middleware-validation": "^1.0.1",
|
|
46
53
|
"express": "^5.0.0",
|
|
47
54
|
"zod": "^4.0.0"
|
|
48
55
|
}
|