taleem-kernel 1.3.0 → 1.5.0

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 CHANGED
@@ -1,505 +1,285 @@
1
-
2
-
3
- # Taleem Kernel API
1
+ # Taleem Kernel — API Reference
4
2
 
5
3
  ```js
6
4
  import kernel from "taleem-kernel";
7
- ````
8
-
9
- `taleem-kernel` is the shared DB/domain layer for Taleem applications.
10
-
11
- It owns the canonical Taleem Prisma/SQLite schema and provides the domain modules used by Taleem Server, Taleem Studio, and other Taleem applications.
12
-
13
- The kernel is intentionally small: modules expose operations that make sense for Taleem rather than blindly exposing Prisma CRUD.
14
-
15
- ---
16
-
17
- ## Core
18
-
19
- ```js
20
- kernel.db
21
-
22
- kernel.config
23
-
24
- kernel.jwt
25
-
26
- kernel.communicationPolicy
27
-
28
- kernel.shutdown()
29
5
  ```
30
6
 
31
- `kernel.jwt` is the low-level JWT utility used by the User and Admin modules.
7
+ `taleem-kernel` is the shared DB/domain layer for Taleem applications. It owns the canonical Prisma/SQLite schema and exposes domain modules — not raw CRUD — used by Taleem Server, Taleem Studio, and other Taleem apps.
32
8
 
33
- Applications normally should not create or verify application identity tokens directly through `kernel.jwt`. Instead, use the authentication methods on the appropriate domain module.
9
+ This doc is organized for lookup: jump to a module, see its methods, signatures, and gotchas.
34
10
 
35
11
  ---
36
12
 
37
- ## Modules
38
-
39
- ```js
40
- kernel.user
41
-
42
- kernel.admin
43
-
44
- kernel.course
13
+ ## Module Index
14
+
15
+ | Module | Purpose |
16
+ |---|---|
17
+ | `kernel.user` | App user registration/auth |
18
+ | `kernel.admin` | Course-scoped admin accounts |
19
+ | `kernel.course` | Course CRUD + access control |
20
+ | `kernel.group` | Structural grouping within a Course |
21
+ | `kernel.library` | Authored content (articles, players, etc.) |
22
+ | `kernel.communication` | Discussion threads on Library items |
23
+ | `kernel.subscription` | User access to a Course |
24
+ | `kernel.svg` / `kernel.image` / `kernel.audio` | Independent reusable assets |
25
+ | `kernel.jwt` | Low-level token utility (avoid calling directly) |
26
+ | `kernel.db` | Raw Prisma client (escape hatch) |
27
+ | `kernel.config` | Kernel config |
28
+ | `kernel.communicationPolicy` | — |
29
+ | `kernel.shutdown()` | Close DB connection |
30
+
31
+ Relation graph:
45
32
 
46
- kernel.library
47
-
48
- kernel.communication
49
-
50
- kernel.subscription
51
-
52
- kernel.svg
53
-
54
- kernel.image
55
-
56
- kernel.audio
33
+ ```text
34
+ Course
35
+ └─ Group
36
+ └─ Library ─── Communication
37
+ Course ─── Subscription
57
38
  ```
58
39
 
40
+ All relations above are real Prisma relations with `onDelete: Restrict` — nothing cascades. Deleting a parent while children exist throws; detach children first.
41
+
59
42
  ---
60
43
 
61
44
  ## User
62
45
 
63
- Users are the normal Taleem application users.
64
-
65
- User registration and authentication are owned directly by the User module.
66
-
67
- ### User API
68
-
69
46
  ```js
70
47
  kernel.user.list()
71
-
72
48
  kernel.user.get(id)
73
-
74
49
  kernel.user.getByEmail(email)
75
-
76
50
  kernel.user.emailToId(email)
77
-
78
- kernel.user.register(data)
79
-
80
- kernel.user.login(email, password)
81
-
51
+ kernel.user.register(data) // { email, password } → hashes password
52
+ kernel.user.login(email, password) // → JWT string
82
53
  kernel.user.createToken(user)
83
-
84
- kernel.user.authenticate(token)
85
-
54
+ kernel.user.authenticate(token) // → User; throws on wrong token type or invalid token
86
55
  kernel.user.update(id, data)
87
-
88
56
  kernel.user.delete(id)
89
57
  ```
90
58
 
91
- Registration creates a User and hashes the supplied password:
92
-
93
59
  ```js
94
- const user = await kernel.user.register({
95
- email: "student@example.com",
96
- password: "12345678"
97
- });
60
+ const user = await kernel.user.register({ email: "student@example.com", password: "12345678" });
61
+ const token = await kernel.user.login("student@example.com", "12345678");
62
+ const authed = await kernel.user.authenticate(token);
98
63
  ```
99
64
 
100
- Login returns a User JWT:
101
-
102
- ```js
103
- const token = await kernel.user.login(
104
- "student@example.com",
105
- "12345678"
106
- );
107
- ```
108
-
109
- The token can then be authenticated:
110
-
111
- ```js
112
- const user = await kernel.user.authenticate(token);
113
- ```
114
-
115
- User authentication accepts only tokens with:
116
-
117
- ```js
118
- {
119
- type: "user"
120
- }
121
- ```
122
-
123
- The User module creates its own tokens using the kernel JWT utility.
65
+ `authenticate()` only accepts tokens with `type: "user"` — an Admin token is rejected here (and vice versa).
124
66
 
125
67
  ---
126
68
 
127
69
  ## Admin
128
70
 
129
- Admins are independent of Course structure.
130
-
131
- An Admin may be assigned one or more course slugs through `courseSlugs`.
132
-
133
- Admin accounts are provisioned separately from normal User registration.
134
-
135
- ### Admin API
71
+ Admins are independent of Course structure; `courseSlugs` is a **plain string field, not a relation** — deliberately unvalidated since Admin provisioning is script-managed, not app/HTTP-facing.
136
72
 
137
73
  ```js
138
74
  kernel.admin.list()
139
-
140
75
  kernel.admin.get(email)
141
-
142
- kernel.admin.login(email, password)
143
-
76
+ kernel.admin.login(email, password) // → JWT string
144
77
  kernel.admin.createToken(admin)
145
-
146
- kernel.admin.authenticate(token)
147
-
148
- kernel.admin.create(data)
149
-
78
+ kernel.admin.authenticate(token) // → Admin; requires type: "admin"
79
+ kernel.admin.create(data) // { email, password, courseSlugs: JSON.stringify([...]) }
150
80
  kernel.admin.update(email, data)
151
-
152
81
  kernel.admin.delete(email)
153
-
154
- kernel.admin.isAdmin(email, courseSlug)
82
+ kernel.admin.isAdmin(email, courseSlug) // → boolean, course-scoped authorization
155
83
  ```
156
84
 
157
- Admin login returns an Admin JWT:
158
-
159
- ```js
160
- const token = await kernel.admin.login(
161
- "admin@example.com",
162
- "12345678"
163
- );
164
- ```
165
-
166
- The token can then be authenticated:
85
+ Authentication and authorization are separate calls — an authenticated Admin is not automatically authorized for every course:
167
86
 
168
87
  ```js
169
88
  const admin = await kernel.admin.authenticate(token);
89
+ const allowed = await kernel.admin.isAdmin(admin.email, courseSlug); // per-course check
170
90
  ```
171
91
 
172
- Admin authentication accepts only tokens with:
173
-
174
- ```js
175
- {
176
- type: "admin"
177
- }
178
- ```
179
-
180
- Admin authorization is handled separately:
181
-
182
- ```js
183
- const allowed = await kernel.admin.isAdmin(
184
- admin.email,
185
- courseSlug
186
- );
187
- ```
188
-
189
- Authentication and authorization are separate concerns.
190
-
191
- An authenticated Admin is not automatically authorized for every Course.
192
-
193
92
  ---
194
93
 
195
- ## JWT
196
-
197
- JWT is the low-level token utility used by the User and Admin modules.
94
+ ## JWT (low-level — avoid direct use)
198
95
 
199
96
  ```js
200
97
  kernel.jwt.sign(payload)
201
-
202
98
  kernel.jwt.verify(token)
203
99
  ```
204
100
 
205
- User and Admin own their respective identity tokens.
206
-
207
- Applications should normally use:
208
-
209
- ```js
210
- kernel.user.login(...)
211
- kernel.user.authenticate(...)
212
-
213
- kernel.admin.login(...)
214
- kernel.admin.authenticate(...)
215
- ```
216
-
217
- rather than calling `kernel.jwt` directly.
218
-
219
- The important architectural rule is:
220
-
221
- ```text
222
- JWT
223
-
224
- ├── User
225
- │ ├── login()
226
- │ ├── createToken()
227
- │ └── authenticate()
228
-
229
- └── Admin
230
- ├── login()
231
- ├── createToken()
232
- └── authenticate()
233
- ```
234
-
235
- There is no shared identity/authentication module routing between User and Admin.
101
+ Applications should go through `kernel.user.*` / `kernel.admin.*` instead of calling this directly. There is no shared identity module routing between User and Admin each owns its own token lifecycle.
236
102
 
237
103
  ---
238
104
 
239
105
  ## Course
240
106
 
241
- Course is **not a CRUD entity**.
242
-
243
- A Course originates as a static course artifact (`course.json`). The artifact is the source of truth and is materialized into the database by the kernel.
244
-
245
- Internally, a Course is represented by two tables:
246
-
247
- ```text
248
- Course
249
- |
250
- +-- Group
251
- |
252
- +-- items[] // library slugs
253
- ```
254
-
255
- There are intentionally no Prisma relationships between Course, Group, and Library. Their relationship is maintained through Taleem slugs.
256
-
257
- ### Course API
107
+ Course is plain CRUD — created, updated, and retired like any other record. **There is no artifact/seed step.**
258
108
 
259
109
  ```js
260
- kernel.course.list()
261
-
262
- kernel.course.get(courseSlug)
263
-
264
- kernel.course.getGroups(courseSlug)
265
-
266
- kernel.course.getGroup(courseSlug, groupSlug)
110
+ kernel.course.list(filters) // { access, isActive }
111
+ kernel.course.get(slug)
112
+ kernel.course.create(data) // { slug, title, access }
113
+ kernel.course.update(slug, data)
114
+ kernel.course.delete(slug) // throws if Groups or Subscriptions still reference it
115
+ kernel.course.authorize(userId, courseSlug) // → boolean; throws/denies per access tier
116
+ ```
267
117
 
268
- kernel.course.getGroupItems(courseSlug, groupSlug)
118
+ ### Access control
269
119
 
270
- kernel.course.seed(course)
120
+ `Course.access` is one of three tiers:
271
121
 
272
- kernel.course.wipe()
273
- ```
122
+ | Tier | `authorize(null, slug)` | `authorize(userId, slug)` with no subscription | `authorize(userId, slug)` with active subscription |
123
+ |---|---|---|---|
124
+ | `OPEN` | ✅ allowed | ✅ allowed | ✅ allowed |
125
+ | `MEMBERS` | ❌ throws | ✅ allowed (just needs to be logged in) | ✅ allowed |
126
+ | `SUBSCRIPTION` | ❌ throws | ❌ throws | ✅ allowed |
274
127
 
275
- `seed()` materializes a complete course artifact:
128
+ For `SUBSCRIPTION` courses, `course.authorize()` delegates to `kernel.subscription.authorize(userId, courseSlug)` — see Subscription below for exactly what "active" means.
276
129
 
277
- ```js
278
- await kernel.course.seed(course);
279
- ```
130
+ Since a Course with subscription history can't be hard-deleted (Restrict), retiring a course in practice means `update(slug, { isActive: false })`, not `delete()`.
280
131
 
281
- `wipe()` removes the materialized Course and Group data before rebuilding from the source artifacts:
132
+ ---
282
133
 
283
- ```js
284
- await kernel.course.wipe();
285
- ```
134
+ ## Group
286
135
 
287
- There are deliberately no:
136
+ Uniquely identified by the composite key `(courseSlug, slug)`.
288
137
 
289
138
  ```js
290
- course.create()
291
-
292
- course.update()
293
-
294
- course.delete()
139
+ kernel.group.list(filters)
140
+ kernel.group.listByCourse(courseSlug)
141
+ kernel.group.get(courseSlug, slug)
142
+ kernel.group.create(data) // throws if courseSlug doesn't resolve to a Course
143
+ kernel.group.update(courseSlug, slug, data)
144
+ kernel.group.delete(courseSlug, slug) // throws if any Library rows still reference it
295
145
  ```
296
146
 
297
- because a Course is an artifact, not an independently edited CRUD record.
298
-
299
147
  ---
300
148
 
301
149
  ## Library
302
150
 
303
- Library contains the actual authored Taleem content.
304
-
305
- Library items belong logically to a Course and Group through string slugs:
306
-
307
- ```text
308
- Library.courseSlug → Course.slug
309
-
310
- Library.groupSlug → Group.slug
311
-
312
- Library.slug → Group.items[]
313
- ```
314
-
315
- These are application-level relationships, not Prisma foreign keys.
316
-
317
- ### Library API
151
+ The actual authored content. Relates to Course only indirectly, through Group (`library.group.course`).
318
152
 
319
153
  ```js
320
- kernel.library.list()
321
-
322
- kernel.library.listByCourse(courseSlug)
323
-
324
- kernel.library.listByGroup(courseSlug, groupSlug)
325
-
326
- kernel.library.get(slug)
327
-
328
- kernel.library.create(data)
329
-
154
+ kernel.library.list(filters, options)
155
+ kernel.library.listByCourse(courseSlug, options)
156
+ kernel.library.listByGroup(courseSlug, groupSlug, options)
157
+ kernel.library.get(slug, options)
158
+ kernel.library.create(data) // throws if (courseSlug, groupSlug) doesn't resolve to a Group
330
159
  kernel.library.update(slug, data)
331
-
332
- kernel.library.delete(slug)
160
+ kernel.library.delete(slug) // throws if any Communication rows still reference it
161
+ kernel.library.createFromSlot(slug, courseSlug, groupSlug, type)
333
162
  ```
334
163
 
335
164
  Filtering:
336
165
 
337
166
  ```js
338
- kernel.library.list({
339
- type,
340
- courseSlug,
341
- groupSlug
342
- });
167
+ kernel.library.list({ type, courseSlug, groupSlug, status }); // status only applied when includeUnpublished is true
343
168
  ```
344
169
 
345
- `list()` returns the library metadata needed by applications without requiring the full content body.
346
-
347
- Library items also have a lifecycle status:
170
+ ### Lifecycle
348
171
 
349
172
  ```text
350
- DRAFT
351
- PUBLISHED
352
- ARCHIVED
173
+ DRAFT → PUBLISHED → ARCHIVED
353
174
  ```
354
175
 
355
- The default status for a new Library item is:
176
+ New items default to `DRAFT`. **`get()` and `list()` return `PUBLISHED` only by default** — the safe default for public routes. Admin/preview contexts must opt in:
356
177
 
357
- ```text
358
- DRAFT
178
+ ```js
179
+ kernel.library.get(slug, { includeUnpublished: true });
180
+ kernel.library.list({ courseSlug }, { includeUnpublished: true });
359
181
  ```
360
182
 
361
- ---
362
-
363
- ## Assets
364
-
365
- SVG, Image, and Audio are independent reusable assets.
366
-
367
- They are not tied to Courses, Groups, or Library records at the database level.
368
-
369
- Their metadata is intentionally small:
183
+ To retire content that has live discussion, archive it (`update(slug, { status: "ARCHIVED" })`) rather than deleting — delete is blocked while Communication rows exist.
370
184
 
371
- ```text
372
- slug
185
+ ---
373
186
 
374
- title
187
+ ## Communication
375
188
 
376
- tags
377
- ```
378
-
379
- Assets can be discovered through tags and referenced by content where appropriate.
189
+ Discussion threads on a Library item (student questions, teacher notes, comments).
380
190
 
381
191
  ```js
382
- kernel.svg
383
-
384
- kernel.image
385
-
386
- kernel.audio
192
+ kernel.communication.list(filters)
193
+ kernel.communication.get(id) // includes { user, library }
194
+ kernel.communication.create(data) // throws if librarySlug doesn't resolve to a Library row
195
+ kernel.communication.update(id, data)
196
+ kernel.communication.delete(id)
197
+ kernel.communication.listUnanswered(courseSlug) // authorResponse is null or ""
198
+ kernel.communication.countUserOpenQuestions(userId) // → number
387
199
  ```
388
200
 
389
- These modules provide the normal asset operations such as:
201
+ Filtering:
390
202
 
391
203
  ```js
392
- list()
393
-
394
- get()
395
-
396
- create()
397
-
398
- update()
399
-
400
- delete()
204
+ kernel.communication.list({
205
+ courseSlug, // resolved via the Library relation
206
+ librarySlug,
207
+ userId,
208
+ initiator, // STUDENT | TEACHER
209
+ unanswered
210
+ });
401
211
  ```
402
212
 
403
- ---
404
-
405
- ## Schema
213
+ `type` (free-form category, e.g. `"user-comment"`) and `initiator` (`STUDENT` default, or `TEACHER`) are independent axes — don't conflate them.
406
214
 
407
- The kernel owns the **canonical Taleem Prisma/SQLite schema**.
408
-
409
- Applications using the Taleem database should use the kernel rather than maintaining their own DB/domain layer.
410
-
411
- The important architectural rule is:
412
-
413
- ```text
414
- Kernel
415
-
416
-
417
-
418
- Canonical schema + Taleem domain operations
419
-
420
-
421
-
422
- Applications
423
- ```
424
-
425
- Applications should not bypass the kernel for normal Taleem DB operations.
215
+ `get()`/`list()` always include `user` and `library` no second manual query needed.
426
216
 
427
217
  ---
428
218
 
429
- ## Schema Management
219
+ ## Subscription
430
220
 
431
- The kernel provides CLI commands for maintaining schema compatibility:
221
+ A User's access to a Course.
432
222
 
433
- ```bash
434
- npx taleem-kernel schema-check
435
-
436
- npx taleem-kernel schema-update
223
+ ```js
224
+ kernel.subscription.list(filters)
225
+ kernel.subscription.get(id)
226
+ kernel.subscription.create(data) // { userId, courseSlug, startsAt, endsAt }
227
+ kernel.subscription.update(id, data)
228
+ kernel.subscription.delete(id)
229
+ kernel.subscription.authorize(userId, courseSlug) // throws if no ACTIVE subscription
437
230
  ```
438
231
 
439
- `schema-check` compares the application's Prisma schema with the canonical schema shipped with `taleem-kernel`.
232
+ "Active" = `startsAt <= now <= endsAt` for the given `courseSlug`. An expired (`endsAt` in the past) or not-yet-started (`startsAt` in the future) subscription does **not** authorize.
440
233
 
441
- `schema-update` copies the kernel's canonical schema into the application's Prisma schema.
442
-
443
- After updating the schema, run the application's normal Prisma migration/generation process as required.
234
+ A Course cannot be hard-deleted while any subscription history exists for it including expired/cancelled ones.
444
235
 
445
236
  ---
446
237
 
447
- ## Server Dependencies
448
-
449
- `taleem-server` should use `taleem-kernel` for all Taleem database access.
450
-
451
- The server provides the HTTP/API layer on top of the kernel.
238
+ ## Assets — SVG, Image, Audio
452
239
 
453
- The server should **not maintain its own Taleem Prisma models or duplicate DB modules**.
454
-
455
- Authentication at the server layer should use the appropriate Kernel identity module:
240
+ Independent reusable assets. **Not tied to Course, Group, or Library at the DB level** — referenced by slug from content where needed, not by relation.
456
241
 
457
242
  ```js
458
- const user = await kernel.user.authenticate(token);
243
+ kernel.svg.list() / .get(slug) / .create(data) / .update(slug, data) / .delete(slug)
244
+ kernel.image.list() / .get(slug) / .create(data) / .update(slug, data) / .delete(slug)
245
+ kernel.audio.list() / .get(slug) / .create(data) / .update(slug, data) / .delete(slug)
459
246
  ```
460
247
 
461
- or:
248
+ **Field shapes are not identical across the three:**
462
249
 
463
- ```js
464
- const admin = await kernel.admin.authenticate(token);
465
- ```
250
+ | Field | Audio | Image | Svg |
251
+ |---|---|---|---|
252
+ | `slug` | required | required | required |
253
+ | `title` | optional | optional | optional |
254
+ | `tags` | optional | optional | optional |
255
+ | `body` | — | — | **required** |
466
256
 
467
- Course-level Admin authorization can then be checked separately:
257
+ Audio and Image currently have **no file-path field** `create()` will reject unknown keys like `filePath`. How the record maps to an actual file on disk (slug-as-filename convention, or handled at the app layer) is not encoded in the schema — confirm the convention before wiring up upload/serve logic.
468
258
 
469
- ```js
470
- const allowed = await kernel.admin.isAdmin(
471
- admin.email,
472
- courseSlug
473
- );
474
- ```
259
+ Svg stores its content directly in `body` — it's DB-only, no file involved.
475
260
 
476
261
  ---
477
262
 
478
- ## Design Principles
479
-
480
- The kernel follows a few important Taleem-specific rules:
481
-
482
- 1. **Course artifacts are the source of truth.**
483
-
484
- 2. **Course is not generic CRUD.** It is materialized from the course artifact.
485
-
486
- 3. **Course and Group are internally separate tables but one domain concept.**
487
-
488
- 4. **Course, Group, and Library are connected by slugs, not Prisma relationships.**
489
-
490
- 5. **Relational integrity is enforced by the Taleem build/application layer.**
491
-
492
- 6. **Library contains authored content; Course defines the expected structure and item slugs.**
493
-
494
- 7. **Library content has an explicit lifecycle: DRAFT, PUBLISHED, and ARCHIVED.**
495
-
496
- 8. **SVG, Image, and Audio are independent reusable assets.**
497
-
498
- 9. **User and Admin authentication are separate domain concerns.**
263
+ ## Schema Management
499
264
 
500
- 10. **JWT is a low-level utility; User and Admin own their identity token lifecycle.**
265
+ ```bash
266
+ npx taleem-kernel schema-check # diff app's Prisma schema vs kernel's canonical schema
267
+ npx taleem-kernel schema-update # copy kernel's canonical schema into the app
268
+ ```
501
269
 
502
- 11. **The kernel exposes domain operations only where they represent real Taleem behavior.**
270
+ Run the app's normal Prisma migrate/generate after `schema-update`. Applications should not maintain their own Taleem Prisma models — go through the kernel.
503
271
 
504
- The goal is a small, stable kernel that owns the Taleem contract without pretending to be a generic CRUD framework.
272
+ ---
505
273
 
274
+ ## Design Principles (quick reference)
275
+
276
+ 1. Course → Group → Library are real Prisma relations, `onDelete: Restrict` throughout. Nothing cascades.
277
+ 2. Course is plain CRUD — no artifact/seed step.
278
+ 3. Course/Group/Library are separate tables; Library reaches Course only via Group.
279
+ 4. Library has explicit lifecycle (DRAFT/PUBLISHED/ARCHIVED); public reads default to PUBLISHED only.
280
+ 5. Communication relates to Library by a real relation; `type` and `initiator` are independent.
281
+ 6. Subscription relates to Course by a real relation; Course with subscription history can't be hard-deleted.
282
+ 7. `Admin.courseSlugs` is a plain string, not a relation — deliberate, script-managed exception.
283
+ 8. SVG/Image/Audio are independent assets, no relation to Course/Group/Library.
284
+ 9. User and Admin auth are separate domain concerns, each owning its own token lifecycle.
285
+ 10. JWT is low-level; go through User/Admin modules, not `kernel.jwt` directly.
@@ -8,6 +8,7 @@ import User from "./modules/User.js";
8
8
  import Admin from "./modules/Admin.js";
9
9
  import Library from "./modules/Library.js";
10
10
  import Course from "./modules/Course.js";
11
+ import Group from "./modules/Group.js";
11
12
  import Communication from "./modules/Communication.js";
12
13
  import Subscription from "./modules/Subscription.js";
13
14
  import Image from "./modules/Image.js";
@@ -24,6 +25,7 @@ class ServerKernel {
24
25
  this.admin = new Admin(this);
25
26
  this.library = new Library(this);
26
27
  this.course = new Course(this);
28
+ this.group = new Group(this);
27
29
  this.image = new Image(this);
28
30
  this.audio = new Audio(this);
29
31
  this.svg = new Svg(this);