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/bin/schema-check.js +21 -10
- package/bin/schema-update.js +1 -0
- package/package.json +4 -4
- package/prisma/dev.db +0 -0
- package/prisma/migrations/20260824182316_add_library_status/migration.sql +24 -0
- package/prisma/migrations/20260829212536_init/migration.sql +96 -0
- package/prisma/schema.prisma +82 -57
- package/readme.md +244 -56
- package/src/{ServerKernel.js → index.js} +4 -2
- package/src/modules/Admin.js +73 -11
- 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/src/modules/User.js +50 -7
- package/src/utils/JWT.js +31 -0
- package/prisma/content.db +0 -0
- package/src/Auth.js +0 -105
package/readme.md
CHANGED
|
@@ -1,97 +1,285 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
13
|
-
kernel.
|
|
14
|
-
kernel.
|
|
15
|
-
kernel.
|
|
16
|
-
kernel.
|
|
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
|
-
|
|
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.
|
|
23
|
-
kernel.admin
|
|
24
|
-
kernel.
|
|
25
|
-
kernel.
|
|
26
|
-
kernel.
|
|
27
|
-
kernel.
|
|
28
|
-
kernel.
|
|
29
|
-
kernel.
|
|
30
|
-
kernel.
|
|
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
|
-
|
|
85
|
+
Authentication and authorization are separate calls — an authenticated Admin is not automatically authorized for every course:
|
|
34
86
|
|
|
35
87
|
```js
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## JWT (low-level — avoid direct use)
|
|
44
95
|
|
|
45
96
|
```js
|
|
46
|
-
kernel.
|
|
47
|
-
kernel.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
60
|
-
kernel.
|
|
61
|
-
kernel.course.
|
|
62
|
-
kernel.
|
|
63
|
-
kernel.
|
|
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
|
-
|
|
118
|
+
### Access control
|
|
67
119
|
|
|
68
|
-
|
|
120
|
+
`Course.access` is one of three tiers:
|
|
69
121
|
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
147
|
+
---
|
|
78
148
|
|
|
79
|
-
|
|
149
|
+
## Library
|
|
80
150
|
|
|
81
|
-
|
|
151
|
+
The actual authored content. Relates to Course only indirectly, through Group (`library.group.course`).
|
|
82
152
|
|
|
83
|
-
|
|
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
|
-
|
|
164
|
+
Filtering:
|
|
86
165
|
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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);
|
package/src/modules/Admin.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
// src/
|
|
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) {
|
|
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
|
-
|
|
21
|
-
if (!admin
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/modules/Course.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
}
|