taleem-kernel 1.2.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,97 +1,285 @@
1
- ## Taleem Kernel API
1
+ # Taleem Kernel API Reference
2
2
 
3
3
  ```js
4
4
  import kernel from "taleem-kernel";
5
5
  ```
6
6
 
7
- `taleem-kernel` is the shared DB/domain layer for Taleem applications. It provides Prisma/SQLite access and Taleem-specific 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.
8
8
 
9
- ### Core
9
+ This doc is organized for lookup: jump to a module, see its methods, signatures, and gotchas.
10
+
11
+ ---
12
+
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:
32
+
33
+ ```text
34
+ Course
35
+ └─ Group
36
+ └─ Library ─── Communication
37
+ Course ─── Subscription
38
+ ```
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
+
42
+ ---
43
+
44
+ ## User
10
45
 
11
46
  ```js
12
- kernel.db // Prisma client
13
- kernel.config // configuration
14
- kernel.auth // authentication/JWT
15
- kernel.communicationPolicy
16
- kernel.shutdown()
47
+ kernel.user.list()
48
+ kernel.user.get(id)
49
+ kernel.user.getByEmail(email)
50
+ kernel.user.emailToId(email)
51
+ kernel.user.register(data) // { email, password } → hashes password
52
+ kernel.user.login(email, password) // → JWT string
53
+ kernel.user.createToken(user)
54
+ kernel.user.authenticate(token) // → User; throws on wrong token type or invalid token
55
+ kernel.user.update(id, data)
56
+ kernel.user.delete(id)
17
57
  ```
18
58
 
19
- ### Modules
59
+ ```js
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);
63
+ ```
64
+
65
+ `authenticate()` only accepts tokens with `type: "user"` — an Admin token is rejected here (and vice versa).
66
+
67
+ ---
68
+
69
+ ## Admin
70
+
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.
20
72
 
21
73
  ```js
22
- kernel.user
23
- kernel.admin
24
- kernel.course
25
- kernel.library
26
- kernel.communication
27
- kernel.subscription
28
- kernel.svg
29
- kernel.image
30
- kernel.audio
74
+ kernel.admin.list()
75
+ kernel.admin.get(email)
76
+ kernel.admin.login(email, password) // → JWT string
77
+ kernel.admin.createToken(admin)
78
+ kernel.admin.authenticate(token) // → Admin; requires type: "admin"
79
+ kernel.admin.create(data) // { email, password, courseSlugs: JSON.stringify([...]) }
80
+ kernel.admin.update(email, data)
81
+ kernel.admin.delete(email)
82
+ kernel.admin.isAdmin(email, courseSlug) // → boolean, course-scoped authorization
31
83
  ```
32
84
 
33
- Each module provides simple DB/domain operations such as:
85
+ Authentication and authorization are separate calls an authenticated Admin is not automatically authorized for every course:
34
86
 
35
87
  ```js
36
- list()
37
- get(...)
38
- create(data)
39
- update(id, data)
40
- delete(id)
88
+ const admin = await kernel.admin.authenticate(token);
89
+ const allowed = await kernel.admin.isAdmin(admin.email, courseSlug); // per-course check
41
90
  ```
42
91
 
43
- with additional module-specific methods such as:
92
+ ---
93
+
94
+ ## JWT (low-level — avoid direct use)
44
95
 
45
96
  ```js
46
- kernel.user.login()
47
- kernel.user.register()
48
- kernel.admin.isAdmin()
49
- kernel.library.listByCourse()
50
- kernel.communication.listUnanswered()
51
- kernel.communication.countUserOpenQuestions()
97
+ kernel.jwt.sign(payload)
98
+ kernel.jwt.verify(token)
52
99
  ```
53
100
 
54
- ### Schema
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.
102
+
103
+ ---
104
+
105
+ ## Course
55
106
 
56
- The kernel owns the **canonical Taleem Prisma/SQLite schema**. Applications using Taleem DB should use the kernel rather than maintaining their own DB layer.
107
+ Course is plain CRUD created, updated, and retired like any other record. **There is no artifact/seed step.**
57
108
 
58
109
  ```js
59
- kernel.auth.authenticate(token)
60
- kernel.user.login(email, password)
61
- kernel.course.list()
62
- kernel.library.get(slug)
63
- kernel.svg.get(slug)
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
64
116
  ```
65
117
 
66
- `kernel.shutdown()` disconnects Prisma when the application exits.
118
+ ### Access control
67
119
 
68
- ## Schema Management
120
+ `Course.access` is one of three tiers:
69
121
 
70
- The kernel provides two CLI commands for maintaining schema compatibility:
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 |
71
127
 
72
- ```bash
73
- npx taleem-kernel schema-check
74
- npx taleem-kernel schema-update
128
+ For `SUBSCRIPTION` courses, `course.authorize()` delegates to `kernel.subscription.authorize(userId, courseSlug)` — see Subscription below for exactly what "active" means.
129
+
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()`.
131
+
132
+ ---
133
+
134
+ ## Group
135
+
136
+ Uniquely identified by the composite key `(courseSlug, slug)`.
137
+
138
+ ```js
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
75
145
  ```
76
146
 
77
- `schema-check` compares the application's `prisma/schema.prisma` with the canonical schema shipped with `taleem-kernel`.
147
+ ---
78
148
 
79
- `schema-update` copies the kernel's canonical schema into the application's `prisma/schema.prisma`.
149
+ ## Library
80
150
 
81
- After updating the schema, run the application's normal Prisma migration/generation process as required.
151
+ The actual authored content. Relates to Course only indirectly, through Group (`library.group.course`).
82
152
 
83
- ## Server Dependencies
153
+ ```js
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
159
+ kernel.library.update(slug, data)
160
+ kernel.library.delete(slug) // throws if any Communication rows still reference it
161
+ kernel.library.createFromSlot(slug, courseSlug, groupSlug, type)
162
+ ```
84
163
 
85
- `taleem-server` should use `taleem-kernel` for all Taleem database access.
164
+ Filtering:
86
165
 
87
- The server provides the HTTP/API layer on top of the kernel:
166
+ ```js
167
+ kernel.library.list({ type, courseSlug, groupSlug, status }); // status only applied when includeUnpublished is true
168
+ ```
169
+
170
+ ### Lifecycle
88
171
 
89
172
  ```text
90
- "@prisma/client": "^6.19.3",
91
- "bcrypt": "^6.0.0",
92
- "dotenv": "^17.4.2",
93
- "jsonwebtoken": "^9.0.3",
94
- "zod": "^4.4.3"
173
+ DRAFT → PUBLISHED → ARCHIVED
174
+ ```
175
+
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:
177
+
178
+ ```js
179
+ kernel.library.get(slug, { includeUnpublished: true });
180
+ kernel.library.list({ courseSlug }, { includeUnpublished: true });
181
+ ```
182
+
183
+ To retire content that has live discussion, archive it (`update(slug, { status: "ARCHIVED" })`) rather than deleting — delete is blocked while Communication rows exist.
184
+
185
+ ---
186
+
187
+ ## Communication
188
+
189
+ Discussion threads on a Library item (student questions, teacher notes, comments).
190
+
191
+ ```js
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
199
+ ```
200
+
201
+ Filtering:
202
+
203
+ ```js
204
+ kernel.communication.list({
205
+ courseSlug, // resolved via the Library relation
206
+ librarySlug,
207
+ userId,
208
+ initiator, // STUDENT | TEACHER
209
+ unanswered
210
+ });
211
+ ```
212
+
213
+ `type` (free-form category, e.g. `"user-comment"`) and `initiator` (`STUDENT` default, or `TEACHER`) are independent axes — don't conflate them.
214
+
215
+ `get()`/`list()` always include `user` and `library` — no second manual query needed.
216
+
217
+ ---
218
+
219
+ ## Subscription
220
+
221
+ A User's access to a Course.
222
+
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
230
+ ```
231
+
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.
233
+
234
+ A Course cannot be hard-deleted while any subscription history exists for it — including expired/cancelled ones.
235
+
236
+ ---
237
+
238
+ ## Assets — SVG, Image, Audio
239
+
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.
241
+
242
+ ```js
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)
95
246
  ```
96
247
 
97
- The server should **not maintain its own Taleem Prisma models or DB modules**.
248
+ **Field shapes are not identical across the three:**
249
+
250
+ | Field | Audio | Image | Svg |
251
+ |---|---|---|---|
252
+ | `slug` | required | required | required |
253
+ | `title` | optional | optional | optional |
254
+ | `tags` | optional | optional | optional |
255
+ | `body` | — | — | **required** |
256
+
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.
258
+
259
+ Svg stores its content directly in `body` — it's DB-only, no file involved.
260
+
261
+ ---
262
+
263
+ ## Schema Management
264
+
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
+ ```
269
+
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.
271
+
272
+ ---
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.
@@ -2,12 +2,13 @@
2
2
 
3
3
  import { PrismaClient } from "@prisma/client";
4
4
  import Config from "./Config.js";
5
- import Auth from "./Auth.js";
5
+ // import Auth from "./Auth.js";
6
6
  import CommunicationPolicy from "./CommunicationPolicy.js";
7
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";
@@ -18,12 +19,13 @@ class ServerKernel {
18
19
  constructor() {
19
20
  this.config = new Config();
20
21
  this.db = new PrismaClient();
21
- this.auth = new Auth(this);
22
+ // this.auth = new Auth(this);
22
23
  this.communicationPolicy = new CommunicationPolicy(this);
23
24
  this.user = new User(this);
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);
@@ -1,9 +1,13 @@
1
- // src/serverKernel/modules/Admin.js
1
+ // /home/bilal-tariq/00--TALEEM/taleem-kernel/src/modules/Admin.js
2
2
 
3
3
  import bcrypt from "bcrypt";
4
+ import JWT from "../utils/JWT.js";
4
5
 
5
6
  export default class Admin {
6
- constructor(kernel) { this.kernel = kernel; }
7
+ constructor(kernel) {
8
+ this.kernel = kernel;
9
+ this.jwt = new JWT(kernel);
10
+ }
7
11
 
8
12
  async list(filters = {}) {
9
13
  const where = {};
@@ -15,37 +19,95 @@ export default class Admin {
15
19
  return this.kernel.db.admin.findUnique({ where: { email } });
16
20
  }
17
21
 
22
+ // Authentication
23
+
18
24
  async login(email, password) {
19
25
  const admin = await this.get(email);
20
- if (!admin) throw new Error(`Admin.login(): Admin '${email}' not found.`);
21
- if (!admin.isActive) throw new Error(`Admin.login(): Admin '${email}' is inactive.`);
26
+
27
+ if (!admin) {
28
+ throw new Error(`Admin.login(): Admin '${email}' not found.`);
29
+ }
30
+
31
+ if (!admin.isActive) {
32
+ throw new Error(`Admin.login(): Admin '${email}' is inactive.`);
33
+ }
22
34
 
23
35
  const ok = await bcrypt.compare(password, admin.password);
24
- if (!ok) throw new Error(`Admin.login(): Invalid password.`);
25
36
 
26
- return this.kernel.auth.createAdminToken(admin);
37
+ if (!ok) {
38
+ throw new Error(`Admin.login(): Invalid password.`);
39
+ }
40
+
41
+ return this.createToken(admin);
42
+ }
43
+
44
+ async createToken(admin) {
45
+ return this.jwt.sign({
46
+ email: admin.email,
47
+ type: "admin"
48
+ });
49
+ }
50
+
51
+ async authenticate(token) {
52
+ const payload = this.jwt.verify(token);
53
+
54
+ if (payload.type !== "admin") {
55
+ throw new Error("Admin token required.");
56
+ }
57
+
58
+ const admin = await this.get(payload.email);
59
+
60
+ if (!admin) {
61
+ throw new Error("Admin not found.");
62
+ }
63
+
64
+ if (!admin.isActive) {
65
+ throw new Error("Admin is inactive.");
66
+ }
67
+
68
+ return admin;
27
69
  }
28
70
 
71
+ // CRUD
72
+
29
73
  async create(data) {
30
74
  const createData = { ...data };
31
- if (createData.password) createData.password = await bcrypt.hash(createData.password, 10);
32
- return this.kernel.db.admin.create({ data: createData });
75
+
76
+ if (createData.password) {
77
+ createData.password = await bcrypt.hash(createData.password, 10);
78
+ }
79
+
80
+ return this.kernel.db.admin.create({
81
+ data: createData
82
+ });
33
83
  }
34
84
 
35
85
  async update(email, data) {
36
86
  const updateData = { ...data };
37
- if (updateData.password) updateData.password = await bcrypt.hash(updateData.password, 10);
38
- return this.kernel.db.admin.update({ where: { email }, data: updateData });
87
+
88
+ if (updateData.password) {
89
+ updateData.password = await bcrypt.hash(updateData.password, 10);
90
+ }
91
+
92
+ return this.kernel.db.admin.update({
93
+ where: { email },
94
+ data: updateData
95
+ });
39
96
  }
40
97
 
41
98
  async delete(email) {
42
- return this.kernel.db.admin.delete({ where: { email } });
99
+ return this.kernel.db.admin.delete({
100
+ where: { email }
101
+ });
43
102
  }
44
103
 
45
104
  async isAdmin(email, courseSlug) {
46
105
  const admin = await this.get(email);
106
+
47
107
  if (!admin) return false;
108
+
48
109
  const courseSlugs = JSON.parse(admin.courseSlugs || "[]");
110
+
49
111
  return courseSlugs.includes(courseSlug);
50
112
  }
51
113
  }
@@ -8,18 +8,12 @@ export default class Communication {
8
8
  const where = {};
9
9
 
10
10
  if (filters.courseSlug) {
11
- const items = await this.kernel.db.library.findMany({
12
- where: { courseSlug: filters.courseSlug },
13
- select: { slug: true }
14
- });
15
-
16
- where.librarySlug = {
17
- in: items.map(item => item.slug)
18
- };
11
+ where.library = { courseSlug: filters.courseSlug };
19
12
  }
20
13
 
21
14
  if (filters.librarySlug) where.librarySlug = filters.librarySlug;
22
15
  if (filters.userId) where.userId = filters.userId;
16
+ if (filters.initiator) where.initiator = filters.initiator;
23
17
 
24
18
  if (filters.unanswered) {
25
19
  where.OR = [
@@ -30,35 +24,20 @@ export default class Communication {
30
24
 
31
25
  return this.kernel.db.communication.findMany({
32
26
  where,
33
- include: { user: true },
27
+ include: { user: true, library: true },
34
28
  orderBy: { createdAt: "desc" }
35
29
  });
36
30
  }
37
31
 
38
32
  async get(id) {
39
- const item = await this.kernel.db.communication.findUnique({
33
+ return this.kernel.db.communication.findUnique({
40
34
  where: { id },
41
- include: { user: true }
35
+ include: { user: true, library: true }
42
36
  });
43
-
44
- if (!item) return null;
45
-
46
- const library = await this.kernel.db.library.findUnique({
47
- where: { slug: item.librarySlug },
48
- select: {
49
- slug: true,
50
- title: true,
51
- courseSlug: true
52
- }
53
- });
54
-
55
- return {
56
- ...item,
57
- library
58
- };
59
37
  }
60
38
 
61
39
  async create(data) {
40
+ // throws if librarySlug doesn't resolve to an existing Library row
62
41
  return this.kernel.db.communication.create({ data });
63
42
  }
64
43
 
@@ -76,30 +55,7 @@ export default class Communication {
76
55
  }
77
56
 
78
57
  async listUnanswered(courseSlug) {
79
- const items = await this.list({
80
- courseSlug,
81
- unanswered: true
82
- });
83
-
84
- const slugs = [...new Set(items.map(item => item.librarySlug))];
85
-
86
- const libraries = await this.kernel.db.library.findMany({
87
- where: { slug: { in: slugs } },
88
- select: {
89
- slug: true,
90
- title: true,
91
- courseSlug: true
92
- }
93
- });
94
-
95
- const map = new Map(
96
- libraries.map(item => [item.slug, item])
97
- );
98
-
99
- return items.map(item => ({
100
- ...item,
101
- library: map.get(item.librarySlug) || null
102
- }));
58
+ return this.list({ courseSlug, unanswered: true });
103
59
  }
104
60
 
105
61
  async countUserOpenQuestions(userId) {
@@ -4,51 +4,44 @@ export default class Course {
4
4
  async list(filters = {}) {
5
5
  const where = {};
6
6
  if (filters.access) where.access = filters.access;
7
+ if (filters.isActive !== undefined) where.isActive = filters.isActive;
7
8
  return this.kernel.db.course.findMany({ where });
8
9
  }
10
+
9
11
  async get(slug) {
10
12
  return this.kernel.db.course.findUnique({ where: { slug } });
11
13
  }
12
- async getGroups(courseSlug) {
13
- return this.kernel.db.group.findMany({
14
- where: { courseSlug },
15
- orderBy: { id: 'asc' }
16
- });
17
- }
18
- async getGroup(courseSlug, groupSlug) {
19
- return this.kernel.db.group.findUnique({
20
- where: {
21
- courseSlug_slug: { courseSlug, slug: groupSlug }
22
- }
23
- });
24
- }
25
- async getGroupItems(courseSlug, groupSlug) {
26
- const group = await this.getGroup(courseSlug, groupSlug);
27
- return group?.items ?? [];
28
- }
29
- async wipe() {
30
- await this.kernel.db.group.deleteMany();
31
- await this.kernel.db.course.deleteMany();
32
- }
33
- async seed(course) {
34
- const { groupings = [], ...courseData } = course;
35
-
36
- const created = await this.kernel.db.course.create({
37
- data: courseData
38
- });
39
-
40
- for (const group of groupings) {
41
- await this.kernel.db.group.create({
42
- data: {
43
- courseSlug: created.slug,
44
- slug: group.slug,
45
- title: group.title,
46
- thumbnail: group.thumbnail ?? null,
47
- items: group.items ?? []
48
- }
49
- });
50
- }
51
-
52
- return created;
14
+
15
+ async create(data) {
16
+ return this.kernel.db.course.create({ data });
17
+ }
18
+
19
+ async update(slug, data) {
20
+ return this.kernel.db.course.update({ where: { slug }, data });
21
+ }
22
+
23
+ async delete(slug) {
24
+ // throws if any Group or Subscription still references this course
25
+ return this.kernel.db.course.delete({ where: { slug } });
26
+ }
27
+
28
+ async authorize(userId, courseSlug) {
29
+ const course = await this.get(courseSlug);
30
+ if (!course) throw new Error(`Course "${courseSlug}" not found.`);
31
+
32
+ if (course.access === "OPEN") return true;
33
+
34
+ if (course.access === "MEMBERS") {
35
+ if (!userId) throw new Error("Login required for this course.");
36
+ return true;
37
+ }
38
+
39
+ if (course.access === "SUBSCRIPTION") {
40
+ if (!userId) throw new Error("Login required for this course.");
41
+ await this.kernel.subscription.authorize(userId, courseSlug);
42
+ return true;
43
+ }
44
+
45
+ throw new Error(`Unknown access level "${course.access}".`);
53
46
  }
54
47
  }