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/bin/schema-check.js +21 -10
- package/bin/schema-update.js +1 -0
- package/package.json +3 -3
- package/prisma/dev.db +0 -0
- package/prisma/migrations/20260829212536_init/migration.sql +96 -0
- package/prisma/schema.prisma +48 -26
- package/readme.md +151 -371
- package/src/{ServerKernel.js → index.js} +2 -0
- package/src/modules/Communication.js +7 -51
- package/src/modules/Course.js +34 -41
- package/src/modules/Group.js +48 -0
- package/src/modules/Library.js +20 -8
- package/src/modules/Subscription.js +12 -61
- package/src/modules/Svg.js +3 -21
- package/prisma/content.db +0 -0
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
|
|
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
|
-
|
|
9
|
+
This doc is organized for lookup: jump to a module, see its methods, signatures, and gotchas.
|
|
34
10
|
|
|
35
11
|
---
|
|
36
12
|
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
kernel.admin
|
|
43
|
-
|
|
44
|
-
kernel.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 **
|
|
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.
|
|
263
|
-
|
|
264
|
-
kernel.course.
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
118
|
+
### Access control
|
|
269
119
|
|
|
270
|
-
|
|
120
|
+
`Course.access` is one of three tiers:
|
|
271
121
|
|
|
272
|
-
|
|
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
|
-
`
|
|
128
|
+
For `SUBSCRIPTION` courses, `course.authorize()` delegates to `kernel.subscription.authorize(userId, courseSlug)` — see Subscription below for exactly what "active" means.
|
|
276
129
|
|
|
277
|
-
|
|
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
|
-
|
|
132
|
+
---
|
|
282
133
|
|
|
283
|
-
|
|
284
|
-
await kernel.course.wipe();
|
|
285
|
-
```
|
|
134
|
+
## Group
|
|
286
135
|
|
|
287
|
-
|
|
136
|
+
Uniquely identified by the composite key `(courseSlug, slug)`.
|
|
288
137
|
|
|
289
138
|
```js
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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.
|
|
323
|
-
|
|
324
|
-
kernel.library.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
358
|
-
|
|
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
|
-
|
|
372
|
-
slug
|
|
185
|
+
---
|
|
373
186
|
|
|
374
|
-
|
|
187
|
+
## Communication
|
|
375
188
|
|
|
376
|
-
|
|
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.
|
|
383
|
-
|
|
384
|
-
kernel.
|
|
385
|
-
|
|
386
|
-
kernel.
|
|
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
|
-
|
|
201
|
+
Filtering:
|
|
390
202
|
|
|
391
203
|
```js
|
|
392
|
-
list(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
219
|
+
## Subscription
|
|
430
220
|
|
|
431
|
-
|
|
221
|
+
A User's access to a Course.
|
|
432
222
|
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
**Field shapes are not identical across the three:**
|
|
462
249
|
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|