@hasna/microservices 0.0.9 → 0.0.11
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/bin/index.js +236 -36
- package/bin/mcp.js +153 -4
- package/dist/index.js +120 -3
- package/microservices/microservice-analytics/package.json +27 -0
- package/microservices/microservice-analytics/src/cli/index.ts +373 -0
- package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
- package/microservices/microservice-analytics/src/db/database.ts +93 -0
- package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
- package/microservices/microservice-analytics/src/index.ts +37 -0
- package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
- package/microservices/microservice-assets/package.json +27 -0
- package/microservices/microservice-assets/src/cli/index.ts +375 -0
- package/microservices/microservice-assets/src/db/assets.ts +370 -0
- package/microservices/microservice-assets/src/db/database.ts +93 -0
- package/microservices/microservice-assets/src/db/migrations.ts +51 -0
- package/microservices/microservice-assets/src/index.ts +32 -0
- package/microservices/microservice-assets/src/mcp/index.ts +346 -0
- package/microservices/microservice-compliance/package.json +27 -0
- package/microservices/microservice-compliance/src/cli/index.ts +467 -0
- package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
- package/microservices/microservice-compliance/src/db/database.ts +93 -0
- package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
- package/microservices/microservice-compliance/src/index.ts +46 -0
- package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
- package/microservices/microservice-habits/package.json +27 -0
- package/microservices/microservice-habits/src/cli/index.ts +315 -0
- package/microservices/microservice-habits/src/db/database.ts +93 -0
- package/microservices/microservice-habits/src/db/habits.ts +451 -0
- package/microservices/microservice-habits/src/db/migrations.ts +46 -0
- package/microservices/microservice-habits/src/index.ts +31 -0
- package/microservices/microservice-habits/src/mcp/index.ts +313 -0
- package/microservices/microservice-health/package.json +27 -0
- package/microservices/microservice-health/src/cli/index.ts +484 -0
- package/microservices/microservice-health/src/db/database.ts +93 -0
- package/microservices/microservice-health/src/db/health.ts +708 -0
- package/microservices/microservice-health/src/db/migrations.ts +70 -0
- package/microservices/microservice-health/src/index.ts +63 -0
- package/microservices/microservice-health/src/mcp/index.ts +437 -0
- package/microservices/microservice-leads/package.json +27 -0
- package/microservices/microservice-leads/src/cli/index.ts +596 -0
- package/microservices/microservice-leads/src/db/database.ts +93 -0
- package/microservices/microservice-leads/src/db/leads.ts +520 -0
- package/microservices/microservice-leads/src/db/lists.ts +151 -0
- package/microservices/microservice-leads/src/db/migrations.ts +93 -0
- package/microservices/microservice-leads/src/index.ts +65 -0
- package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
- package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
- package/microservices/microservice-leads/src/mcp/index.ts +533 -0
- package/microservices/microservice-notifications/package.json +27 -0
- package/microservices/microservice-notifications/src/cli/index.ts +349 -0
- package/microservices/microservice-notifications/src/db/database.ts +93 -0
- package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
- package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
- package/microservices/microservice-notifications/src/index.ts +41 -0
- package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
- package/microservices/microservice-products/package.json +27 -0
- package/microservices/microservice-products/src/cli/index.ts +416 -0
- package/microservices/microservice-products/src/db/categories.ts +154 -0
- package/microservices/microservice-products/src/db/database.ts +93 -0
- package/microservices/microservice-products/src/db/migrations.ts +58 -0
- package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
- package/microservices/microservice-products/src/db/products.ts +452 -0
- package/microservices/microservice-products/src/index.ts +53 -0
- package/microservices/microservice-products/src/mcp/index.ts +453 -0
- package/microservices/microservice-projects/package.json +27 -0
- package/microservices/microservice-projects/src/cli/index.ts +480 -0
- package/microservices/microservice-projects/src/db/database.ts +93 -0
- package/microservices/microservice-projects/src/db/migrations.ts +65 -0
- package/microservices/microservice-projects/src/db/projects.ts +715 -0
- package/microservices/microservice-projects/src/index.ts +57 -0
- package/microservices/microservice-projects/src/mcp/index.ts +501 -0
- package/microservices/microservice-proposals/package.json +27 -0
- package/microservices/microservice-proposals/src/cli/index.ts +400 -0
- package/microservices/microservice-proposals/src/db/database.ts +93 -0
- package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
- package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
- package/microservices/microservice-proposals/src/index.ts +37 -0
- package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
- package/microservices/microservice-reading/package.json +27 -0
- package/microservices/microservice-reading/src/cli/index.ts +464 -0
- package/microservices/microservice-reading/src/db/database.ts +93 -0
- package/microservices/microservice-reading/src/db/migrations.ts +59 -0
- package/microservices/microservice-reading/src/db/reading.ts +524 -0
- package/microservices/microservice-reading/src/index.ts +51 -0
- package/microservices/microservice-reading/src/mcp/index.ts +368 -0
- package/microservices/microservice-travel/package.json +27 -0
- package/microservices/microservice-travel/src/cli/index.ts +505 -0
- package/microservices/microservice-travel/src/db/database.ts +93 -0
- package/microservices/microservice-travel/src/db/migrations.ts +77 -0
- package/microservices/microservice-travel/src/db/travel.ts +802 -0
- package/microservices/microservice-travel/src/index.ts +60 -0
- package/microservices/microservice-travel/src/mcp/index.ts +495 -0
- package/microservices/microservice-wiki/package.json +27 -0
- package/microservices/microservice-wiki/src/cli/index.ts +345 -0
- package/microservices/microservice-wiki/src/db/database.ts +93 -0
- package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
- package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
- package/microservices/microservice-wiki/src/index.ts +32 -0
- package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
- package/package.json +1 -1
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading CRUD operations — books, highlights, reading sessions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// ============ Types ============
|
|
8
|
+
|
|
9
|
+
export interface Book {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
author: string | null;
|
|
13
|
+
isbn: string | null;
|
|
14
|
+
status: "to_read" | "reading" | "completed" | "abandoned";
|
|
15
|
+
rating: number | null;
|
|
16
|
+
category: string | null;
|
|
17
|
+
pages: number | null;
|
|
18
|
+
current_page: number;
|
|
19
|
+
started_at: string | null;
|
|
20
|
+
finished_at: string | null;
|
|
21
|
+
cover_url: string | null;
|
|
22
|
+
metadata: Record<string, unknown>;
|
|
23
|
+
created_at: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface BookRow {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
author: string | null;
|
|
31
|
+
isbn: string | null;
|
|
32
|
+
status: string;
|
|
33
|
+
rating: number | null;
|
|
34
|
+
category: string | null;
|
|
35
|
+
pages: number | null;
|
|
36
|
+
current_page: number;
|
|
37
|
+
started_at: string | null;
|
|
38
|
+
finished_at: string | null;
|
|
39
|
+
cover_url: string | null;
|
|
40
|
+
metadata: string;
|
|
41
|
+
created_at: string;
|
|
42
|
+
updated_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rowToBook(row: BookRow): Book {
|
|
46
|
+
return {
|
|
47
|
+
...row,
|
|
48
|
+
status: row.status as Book["status"],
|
|
49
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Highlight {
|
|
54
|
+
id: string;
|
|
55
|
+
book_id: string;
|
|
56
|
+
text: string;
|
|
57
|
+
page: number | null;
|
|
58
|
+
chapter: string | null;
|
|
59
|
+
color: string;
|
|
60
|
+
notes: string | null;
|
|
61
|
+
created_at: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ReadingSession {
|
|
65
|
+
id: string;
|
|
66
|
+
book_id: string;
|
|
67
|
+
pages_read: number | null;
|
|
68
|
+
duration_min: number | null;
|
|
69
|
+
logged_at: string;
|
|
70
|
+
created_at: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============ Book CRUD ============
|
|
74
|
+
|
|
75
|
+
export interface CreateBookInput {
|
|
76
|
+
title: string;
|
|
77
|
+
author?: string;
|
|
78
|
+
isbn?: string;
|
|
79
|
+
status?: "to_read" | "reading" | "completed" | "abandoned";
|
|
80
|
+
rating?: number;
|
|
81
|
+
category?: string;
|
|
82
|
+
pages?: number;
|
|
83
|
+
current_page?: number;
|
|
84
|
+
started_at?: string;
|
|
85
|
+
finished_at?: string;
|
|
86
|
+
cover_url?: string;
|
|
87
|
+
metadata?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createBook(input: CreateBookInput): Book {
|
|
91
|
+
const db = getDatabase();
|
|
92
|
+
const id = crypto.randomUUID();
|
|
93
|
+
const metadata = JSON.stringify(input.metadata || {});
|
|
94
|
+
|
|
95
|
+
db.prepare(
|
|
96
|
+
`INSERT INTO books (id, title, author, isbn, status, rating, category, pages, current_page, started_at, finished_at, cover_url, metadata)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
98
|
+
).run(
|
|
99
|
+
id,
|
|
100
|
+
input.title,
|
|
101
|
+
input.author || null,
|
|
102
|
+
input.isbn || null,
|
|
103
|
+
input.status || "to_read",
|
|
104
|
+
input.rating ?? null,
|
|
105
|
+
input.category || null,
|
|
106
|
+
input.pages ?? null,
|
|
107
|
+
input.current_page ?? 0,
|
|
108
|
+
input.started_at || null,
|
|
109
|
+
input.finished_at || null,
|
|
110
|
+
input.cover_url || null,
|
|
111
|
+
metadata
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return getBook(id)!;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getBook(id: string): Book | null {
|
|
118
|
+
const db = getDatabase();
|
|
119
|
+
const row = db.prepare("SELECT * FROM books WHERE id = ?").get(id) as BookRow | null;
|
|
120
|
+
return row ? rowToBook(row) : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ListBooksOptions {
|
|
124
|
+
search?: string;
|
|
125
|
+
status?: string;
|
|
126
|
+
category?: string;
|
|
127
|
+
author?: string;
|
|
128
|
+
limit?: number;
|
|
129
|
+
offset?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function listBooks(options: ListBooksOptions = {}): Book[] {
|
|
133
|
+
const db = getDatabase();
|
|
134
|
+
const conditions: string[] = [];
|
|
135
|
+
const params: unknown[] = [];
|
|
136
|
+
|
|
137
|
+
if (options.search) {
|
|
138
|
+
conditions.push("(title LIKE ? OR author LIKE ? OR isbn LIKE ? OR category LIKE ?)");
|
|
139
|
+
const q = `%${options.search}%`;
|
|
140
|
+
params.push(q, q, q, q);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (options.status) {
|
|
144
|
+
conditions.push("status = ?");
|
|
145
|
+
params.push(options.status);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (options.category) {
|
|
149
|
+
conditions.push("category = ?");
|
|
150
|
+
params.push(options.category);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (options.author) {
|
|
154
|
+
conditions.push("author LIKE ?");
|
|
155
|
+
params.push(`%${options.author}%`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let sql = "SELECT * FROM books";
|
|
159
|
+
if (conditions.length > 0) {
|
|
160
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
161
|
+
}
|
|
162
|
+
sql += " ORDER BY updated_at DESC";
|
|
163
|
+
|
|
164
|
+
if (options.limit) {
|
|
165
|
+
sql += " LIMIT ?";
|
|
166
|
+
params.push(options.limit);
|
|
167
|
+
}
|
|
168
|
+
if (options.offset) {
|
|
169
|
+
sql += " OFFSET ?";
|
|
170
|
+
params.push(options.offset);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const rows = db.prepare(sql).all(...params) as BookRow[];
|
|
174
|
+
return rows.map(rowToBook);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface UpdateBookInput {
|
|
178
|
+
title?: string;
|
|
179
|
+
author?: string;
|
|
180
|
+
isbn?: string;
|
|
181
|
+
status?: "to_read" | "reading" | "completed" | "abandoned";
|
|
182
|
+
rating?: number;
|
|
183
|
+
category?: string;
|
|
184
|
+
pages?: number;
|
|
185
|
+
current_page?: number;
|
|
186
|
+
started_at?: string;
|
|
187
|
+
finished_at?: string;
|
|
188
|
+
cover_url?: string;
|
|
189
|
+
metadata?: Record<string, unknown>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function updateBook(id: string, input: UpdateBookInput): Book | null {
|
|
193
|
+
const db = getDatabase();
|
|
194
|
+
const existing = getBook(id);
|
|
195
|
+
if (!existing) return null;
|
|
196
|
+
|
|
197
|
+
const sets: string[] = [];
|
|
198
|
+
const params: unknown[] = [];
|
|
199
|
+
|
|
200
|
+
if (input.title !== undefined) { sets.push("title = ?"); params.push(input.title); }
|
|
201
|
+
if (input.author !== undefined) { sets.push("author = ?"); params.push(input.author); }
|
|
202
|
+
if (input.isbn !== undefined) { sets.push("isbn = ?"); params.push(input.isbn); }
|
|
203
|
+
if (input.status !== undefined) { sets.push("status = ?"); params.push(input.status); }
|
|
204
|
+
if (input.rating !== undefined) { sets.push("rating = ?"); params.push(input.rating); }
|
|
205
|
+
if (input.category !== undefined) { sets.push("category = ?"); params.push(input.category); }
|
|
206
|
+
if (input.pages !== undefined) { sets.push("pages = ?"); params.push(input.pages); }
|
|
207
|
+
if (input.current_page !== undefined) { sets.push("current_page = ?"); params.push(input.current_page); }
|
|
208
|
+
if (input.started_at !== undefined) { sets.push("started_at = ?"); params.push(input.started_at); }
|
|
209
|
+
if (input.finished_at !== undefined) { sets.push("finished_at = ?"); params.push(input.finished_at); }
|
|
210
|
+
if (input.cover_url !== undefined) { sets.push("cover_url = ?"); params.push(input.cover_url); }
|
|
211
|
+
if (input.metadata !== undefined) { sets.push("metadata = ?"); params.push(JSON.stringify(input.metadata)); }
|
|
212
|
+
|
|
213
|
+
if (sets.length === 0) return existing;
|
|
214
|
+
|
|
215
|
+
sets.push("updated_at = datetime('now')");
|
|
216
|
+
params.push(id);
|
|
217
|
+
|
|
218
|
+
db.prepare(`UPDATE books SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
219
|
+
|
|
220
|
+
return getBook(id);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function deleteBook(id: string): boolean {
|
|
224
|
+
const db = getDatabase();
|
|
225
|
+
const result = db.prepare("DELETE FROM books WHERE id = ?").run(id);
|
|
226
|
+
return result.changes > 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function searchBooks(query: string): Book[] {
|
|
230
|
+
return listBooks({ search: query });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function countBooks(): number {
|
|
234
|
+
const db = getDatabase();
|
|
235
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM books").get() as { count: number };
|
|
236
|
+
return row.count;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============ Status Transitions ============
|
|
240
|
+
|
|
241
|
+
export function startBook(id: string): Book | null {
|
|
242
|
+
return updateBook(id, {
|
|
243
|
+
status: "reading",
|
|
244
|
+
started_at: new Date().toISOString(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function finishBook(id: string, rating?: number): Book | null {
|
|
249
|
+
const book = getBook(id);
|
|
250
|
+
if (!book) return null;
|
|
251
|
+
|
|
252
|
+
const input: UpdateBookInput = {
|
|
253
|
+
status: "completed",
|
|
254
|
+
finished_at: new Date().toISOString(),
|
|
255
|
+
current_page: book.pages ?? book.current_page,
|
|
256
|
+
};
|
|
257
|
+
if (rating !== undefined) input.rating = rating;
|
|
258
|
+
|
|
259
|
+
return updateBook(id, input);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function abandonBook(id: string): Book | null {
|
|
263
|
+
return updateBook(id, { status: "abandoned" });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function getCurrentlyReading(): Book[] {
|
|
267
|
+
return listBooks({ status: "reading" });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============ Highlights ============
|
|
271
|
+
|
|
272
|
+
export interface CreateHighlightInput {
|
|
273
|
+
book_id: string;
|
|
274
|
+
text: string;
|
|
275
|
+
page?: number;
|
|
276
|
+
chapter?: string;
|
|
277
|
+
color?: string;
|
|
278
|
+
notes?: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function createHighlight(input: CreateHighlightInput): Highlight {
|
|
282
|
+
const db = getDatabase();
|
|
283
|
+
const id = crypto.randomUUID();
|
|
284
|
+
|
|
285
|
+
db.prepare(
|
|
286
|
+
`INSERT INTO highlights (id, book_id, text, page, chapter, color, notes)
|
|
287
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
288
|
+
).run(
|
|
289
|
+
id,
|
|
290
|
+
input.book_id,
|
|
291
|
+
input.text,
|
|
292
|
+
input.page ?? null,
|
|
293
|
+
input.chapter || null,
|
|
294
|
+
input.color || "yellow",
|
|
295
|
+
input.notes || null
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return getHighlight(id)!;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function getHighlight(id: string): Highlight | null {
|
|
302
|
+
const db = getDatabase();
|
|
303
|
+
return db.prepare("SELECT * FROM highlights WHERE id = ?").get(id) as Highlight | null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function listHighlights(bookId: string): Highlight[] {
|
|
307
|
+
const db = getDatabase();
|
|
308
|
+
return db
|
|
309
|
+
.prepare("SELECT * FROM highlights WHERE book_id = ? ORDER BY page ASC, created_at ASC")
|
|
310
|
+
.all(bookId) as Highlight[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function deleteHighlight(id: string): boolean {
|
|
314
|
+
const db = getDatabase();
|
|
315
|
+
const result = db.prepare("DELETE FROM highlights WHERE id = ?").run(id);
|
|
316
|
+
return result.changes > 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function searchHighlights(query: string): (Highlight & { book_title: string })[] {
|
|
320
|
+
const db = getDatabase();
|
|
321
|
+
const q = `%${query}%`;
|
|
322
|
+
return db
|
|
323
|
+
.prepare(
|
|
324
|
+
`SELECT h.*, b.title as book_title
|
|
325
|
+
FROM highlights h
|
|
326
|
+
JOIN books b ON h.book_id = b.id
|
|
327
|
+
WHERE h.text LIKE ? OR h.notes LIKE ?
|
|
328
|
+
ORDER BY h.created_at DESC`
|
|
329
|
+
)
|
|
330
|
+
.all(q, q) as (Highlight & { book_title: string })[];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============ Reading Sessions ============
|
|
334
|
+
|
|
335
|
+
export interface CreateReadingSessionInput {
|
|
336
|
+
book_id: string;
|
|
337
|
+
pages_read?: number;
|
|
338
|
+
duration_min?: number;
|
|
339
|
+
logged_at: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function createReadingSession(input: CreateReadingSessionInput): ReadingSession {
|
|
343
|
+
const db = getDatabase();
|
|
344
|
+
const id = crypto.randomUUID();
|
|
345
|
+
|
|
346
|
+
db.prepare(
|
|
347
|
+
`INSERT INTO reading_sessions (id, book_id, pages_read, duration_min, logged_at)
|
|
348
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
349
|
+
).run(
|
|
350
|
+
id,
|
|
351
|
+
input.book_id,
|
|
352
|
+
input.pages_read ?? null,
|
|
353
|
+
input.duration_min ?? null,
|
|
354
|
+
input.logged_at
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Update book's current_page if pages_read is provided
|
|
358
|
+
if (input.pages_read) {
|
|
359
|
+
const book = getBook(input.book_id);
|
|
360
|
+
if (book) {
|
|
361
|
+
const newPage = book.current_page + input.pages_read;
|
|
362
|
+
updateBook(input.book_id, { current_page: newPage });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return getReadingSession(id)!;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function getReadingSession(id: string): ReadingSession | null {
|
|
370
|
+
const db = getDatabase();
|
|
371
|
+
return db.prepare("SELECT * FROM reading_sessions WHERE id = ?").get(id) as ReadingSession | null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function listReadingSessions(bookId: string): ReadingSession[] {
|
|
375
|
+
const db = getDatabase();
|
|
376
|
+
return db
|
|
377
|
+
.prepare("SELECT * FROM reading_sessions WHERE book_id = ? ORDER BY logged_at DESC")
|
|
378
|
+
.all(bookId) as ReadingSession[];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function deleteReadingSession(id: string): boolean {
|
|
382
|
+
const db = getDatabase();
|
|
383
|
+
const result = db.prepare("DELETE FROM reading_sessions WHERE id = ?").run(id);
|
|
384
|
+
return result.changes > 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ============ Stats ============
|
|
388
|
+
|
|
389
|
+
export interface ReadingStats {
|
|
390
|
+
books_read: number;
|
|
391
|
+
pages_read: number;
|
|
392
|
+
total_sessions: number;
|
|
393
|
+
avg_rating: number | null;
|
|
394
|
+
by_category: Record<string, number>;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function getReadingStats(year?: number): ReadingStats {
|
|
398
|
+
const db = getDatabase();
|
|
399
|
+
|
|
400
|
+
let yearCondition = "";
|
|
401
|
+
const yearParams: unknown[] = [];
|
|
402
|
+
if (year) {
|
|
403
|
+
yearCondition = " AND strftime('%Y', finished_at) = ?";
|
|
404
|
+
yearParams.push(String(year));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Books read (completed)
|
|
408
|
+
const booksRow = db
|
|
409
|
+
.prepare(`SELECT COUNT(*) as count FROM books WHERE status = 'completed'${yearCondition}`)
|
|
410
|
+
.get(...yearParams) as { count: number };
|
|
411
|
+
|
|
412
|
+
// Avg rating
|
|
413
|
+
const ratingRow = db
|
|
414
|
+
.prepare(`SELECT AVG(rating) as avg FROM books WHERE status = 'completed' AND rating IS NOT NULL${yearCondition}`)
|
|
415
|
+
.get(...yearParams) as { avg: number | null };
|
|
416
|
+
|
|
417
|
+
// Pages read from sessions
|
|
418
|
+
let sessionYearCondition = "";
|
|
419
|
+
const sessionYearParams: unknown[] = [];
|
|
420
|
+
if (year) {
|
|
421
|
+
sessionYearCondition = " WHERE strftime('%Y', logged_at) = ?";
|
|
422
|
+
sessionYearParams.push(String(year));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const pagesRow = db
|
|
426
|
+
.prepare(`SELECT COALESCE(SUM(pages_read), 0) as total FROM reading_sessions${sessionYearCondition}`)
|
|
427
|
+
.get(...sessionYearParams) as { total: number };
|
|
428
|
+
|
|
429
|
+
const sessionsRow = db
|
|
430
|
+
.prepare(`SELECT COUNT(*) as count FROM reading_sessions${sessionYearCondition}`)
|
|
431
|
+
.get(...sessionYearParams) as { count: number };
|
|
432
|
+
|
|
433
|
+
// By category
|
|
434
|
+
const categoryRows = db
|
|
435
|
+
.prepare(
|
|
436
|
+
`SELECT category, COUNT(*) as count FROM books WHERE status = 'completed' AND category IS NOT NULL${yearCondition} GROUP BY category ORDER BY count DESC`
|
|
437
|
+
)
|
|
438
|
+
.all(...yearParams) as { category: string; count: number }[];
|
|
439
|
+
|
|
440
|
+
const by_category: Record<string, number> = {};
|
|
441
|
+
for (const row of categoryRows) {
|
|
442
|
+
by_category[row.category] = row.count;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
books_read: booksRow.count,
|
|
447
|
+
pages_read: pagesRow.total,
|
|
448
|
+
total_sessions: sessionsRow.count,
|
|
449
|
+
avg_rating: ratingRow.avg ? Math.round(ratingRow.avg * 10) / 10 : null,
|
|
450
|
+
by_category,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export interface ReadingPace {
|
|
455
|
+
pages_per_day: number;
|
|
456
|
+
books_per_month: number;
|
|
457
|
+
avg_session_pages: number;
|
|
458
|
+
avg_session_minutes: number | null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function getReadingPace(): ReadingPace {
|
|
462
|
+
const db = getDatabase();
|
|
463
|
+
|
|
464
|
+
// Get first and last session dates to calculate span
|
|
465
|
+
const rangeRow = db
|
|
466
|
+
.prepare(
|
|
467
|
+
`SELECT MIN(logged_at) as first_session, MAX(logged_at) as last_session,
|
|
468
|
+
COALESCE(SUM(pages_read), 0) as total_pages,
|
|
469
|
+
COUNT(*) as total_sessions,
|
|
470
|
+
AVG(pages_read) as avg_pages,
|
|
471
|
+
AVG(duration_min) as avg_min
|
|
472
|
+
FROM reading_sessions`
|
|
473
|
+
)
|
|
474
|
+
.get() as {
|
|
475
|
+
first_session: string | null;
|
|
476
|
+
last_session: string | null;
|
|
477
|
+
total_pages: number;
|
|
478
|
+
total_sessions: number;
|
|
479
|
+
avg_pages: number | null;
|
|
480
|
+
avg_min: number | null;
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
if (!rangeRow.first_session || rangeRow.total_sessions === 0) {
|
|
484
|
+
return { pages_per_day: 0, books_per_month: 0, avg_session_pages: 0, avg_session_minutes: null };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const first = new Date(rangeRow.first_session);
|
|
488
|
+
const last = new Date(rangeRow.last_session!);
|
|
489
|
+
const daySpan = Math.max(1, Math.ceil((last.getTime() - first.getTime()) / (1000 * 60 * 60 * 24)));
|
|
490
|
+
|
|
491
|
+
const pagesPerDay = Math.round((rangeRow.total_pages / daySpan) * 10) / 10;
|
|
492
|
+
|
|
493
|
+
// Books completed in this period
|
|
494
|
+
const booksRow = db
|
|
495
|
+
.prepare(`SELECT COUNT(*) as count FROM books WHERE status = 'completed'`)
|
|
496
|
+
.get() as { count: number };
|
|
497
|
+
|
|
498
|
+
const monthSpan = Math.max(1, daySpan / 30);
|
|
499
|
+
const booksPerMonth = Math.round((booksRow.count / monthSpan) * 10) / 10;
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
pages_per_day: pagesPerDay,
|
|
503
|
+
books_per_month: booksPerMonth,
|
|
504
|
+
avg_session_pages: Math.round((rangeRow.avg_pages || 0) * 10) / 10,
|
|
505
|
+
avg_session_minutes: rangeRow.avg_min ? Math.round(rangeRow.avg_min * 10) / 10 : null,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export interface BookProgress {
|
|
510
|
+
current_page: number;
|
|
511
|
+
total_pages: number | null;
|
|
512
|
+
percentage: number | null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function getBookProgress(bookId: string): BookProgress | null {
|
|
516
|
+
const book = getBook(bookId);
|
|
517
|
+
if (!book) return null;
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
current_page: book.current_page,
|
|
521
|
+
total_pages: book.pages,
|
|
522
|
+
percentage: book.pages ? Math.round((book.current_page / book.pages) * 100) : null,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* microservice-reading — Reading tracker microservice
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createBook,
|
|
7
|
+
getBook,
|
|
8
|
+
listBooks,
|
|
9
|
+
updateBook,
|
|
10
|
+
deleteBook,
|
|
11
|
+
searchBooks,
|
|
12
|
+
countBooks,
|
|
13
|
+
startBook,
|
|
14
|
+
finishBook,
|
|
15
|
+
abandonBook,
|
|
16
|
+
getCurrentlyReading,
|
|
17
|
+
getBookProgress,
|
|
18
|
+
type Book,
|
|
19
|
+
type CreateBookInput,
|
|
20
|
+
type UpdateBookInput,
|
|
21
|
+
type ListBooksOptions,
|
|
22
|
+
type BookProgress,
|
|
23
|
+
} from "./db/reading.js";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
createHighlight,
|
|
27
|
+
getHighlight,
|
|
28
|
+
listHighlights,
|
|
29
|
+
deleteHighlight,
|
|
30
|
+
searchHighlights,
|
|
31
|
+
type Highlight,
|
|
32
|
+
type CreateHighlightInput,
|
|
33
|
+
} from "./db/reading.js";
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
createReadingSession,
|
|
37
|
+
getReadingSession,
|
|
38
|
+
listReadingSessions,
|
|
39
|
+
deleteReadingSession,
|
|
40
|
+
type ReadingSession,
|
|
41
|
+
type CreateReadingSessionInput,
|
|
42
|
+
} from "./db/reading.js";
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
getReadingStats,
|
|
46
|
+
getReadingPace,
|
|
47
|
+
type ReadingStats,
|
|
48
|
+
type ReadingPace,
|
|
49
|
+
} from "./db/reading.js";
|
|
50
|
+
|
|
51
|
+
export { getDatabase, closeDatabase } from "./db/database.js";
|