@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.
Files changed (2) hide show
  1. package/README.md +422 -0
  2. 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.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.10",
43
+ "vitest": "4.1.11",
37
44
  "zod": "4.4.3"
38
45
  },
39
46
  "peerDependencies": {
40
- "@molecule/api-bonds-default-express": "^1.0.0",
41
- "@molecule/api-logger": "^1.0.0",
42
- "@molecule/api-database": "^1.0.0",
43
- "@molecule/api-encryption": "^1.0.0",
44
- "@molecule/api-i18n": "^1.0.0",
45
- "@molecule/api-middleware-validation": "^1.0.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
  }