taleem-kernel 1.0.0 → 1.0.1
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/cli.js +49 -0
- package/bin/schema-check.js +20 -0
- package/bin/schema-update.js +2 -0
- package/package.json +9 -1
- package/prisma/dev.db +0 -0
- package/prisma/migrations/20260823124519_core/migration.sql +118 -0
- package/prisma/migrations/20260823131911_add_audio/migration.sql +8 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +27 -0
- package/readme.md +71 -90
- package/src/ServerKernel.js +16 -114
- package/src/modules/Admin.js +16 -22
- package/src/modules/Audio.js +11 -33
- package/src/modules/Image.js +12 -33
- package/src/modules/Svg.js +12 -30
- package/src/modules/User.js +12 -71
- package/.env +0 -5
- package/src/Logger.js +0 -33
- package/tests/admin.test.js +0 -52
- package/tests/course.test.js +0 -68
- package/tests/kernel.test.js +0 -12
- package/tests/library.test.js +0 -23
package/bin/cli.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const command = process.argv[2];
|
|
8
|
+
|
|
9
|
+
const kernelSchemaPath = path.resolve(
|
|
10
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
11
|
+
"../prisma/schema.prisma"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const appSchemaPath = path.resolve(
|
|
15
|
+
process.cwd(),
|
|
16
|
+
"prisma/schema.prisma"
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (command === "schema-check") {
|
|
20
|
+
const kernelSchema = fs.readFileSync(kernelSchemaPath, "utf8");
|
|
21
|
+
const appSchema = fs.readFileSync(appSchemaPath, "utf8");
|
|
22
|
+
|
|
23
|
+
if (kernelSchema !== appSchema) {
|
|
24
|
+
console.error("❌ Taleem Prisma schema mismatch.");
|
|
25
|
+
console.error(`Kernel: ${kernelSchemaPath}`);
|
|
26
|
+
console.error(`App: ${appSchemaPath}`);
|
|
27
|
+
console.error("Run: npx taleem-kernel schema-update");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log("✓ Taleem Prisma schema is up to date.");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (command === "schema-update") {
|
|
36
|
+
fs.copyFileSync(kernelSchemaPath, appSchemaPath);
|
|
37
|
+
|
|
38
|
+
console.log("✓ Taleem Prisma schema updated.");
|
|
39
|
+
console.log(`Source: ${kernelSchemaPath}`);
|
|
40
|
+
console.log(`Target: ${appSchemaPath}`);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log("Usage: npx taleem-kernel <command>");
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log("Commands:");
|
|
47
|
+
console.log(" schema-check Check Prisma schema");
|
|
48
|
+
console.log(" schema-update Update Prisma schema");
|
|
49
|
+
process.exit(1);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const kernelSchema = fs.readFileSync(
|
|
6
|
+
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../prisma/schema.prisma"),
|
|
7
|
+
"utf8"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const appSchema = fs.readFileSync(
|
|
11
|
+
path.resolve(process.cwd(), "prisma/schema.prisma"),
|
|
12
|
+
"utf8"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
if (kernelSchema !== appSchema) {
|
|
16
|
+
console.error("❌ Taleem Prisma schema mismatch.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log("✓ Taleem Prisma schema is up to date.");
|
package/package.json
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taleem-kernel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Taleem data and business-logic kernel. HTTP is only an adapter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/ServerKernel.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/ServerKernel.js"
|
|
9
9
|
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"taleem-kernel": "./bin/cli.js"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
11
14
|
"test": "vitest run"
|
|
12
15
|
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"prisma",
|
|
19
|
+
"bin"
|
|
20
|
+
],
|
|
13
21
|
"dependencies": {
|
|
14
22
|
"@prisma/client": "^6.19.3",
|
|
15
23
|
"bcrypt": "^6.0.0",
|
package/prisma/dev.db
CHANGED
|
Binary file
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "Course" (
|
|
3
|
+
"slug" TEXT NOT NULL,
|
|
4
|
+
"title" TEXT NOT NULL,
|
|
5
|
+
"description" TEXT,
|
|
6
|
+
"thumbnail" TEXT,
|
|
7
|
+
"access" TEXT NOT NULL DEFAULT 'OPEN',
|
|
8
|
+
"price" INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
"groupings" TEXT NOT NULL DEFAULT '[]'
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
-- CreateTable
|
|
13
|
+
CREATE TABLE "User" (
|
|
14
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
"email" TEXT NOT NULL,
|
|
16
|
+
"password" TEXT NOT NULL,
|
|
17
|
+
"name" TEXT,
|
|
18
|
+
"role" TEXT NOT NULL DEFAULT 'student',
|
|
19
|
+
"resource" TEXT,
|
|
20
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- CreateTable
|
|
24
|
+
CREATE TABLE "Admin" (
|
|
25
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
"email" TEXT NOT NULL,
|
|
27
|
+
"password" TEXT NOT NULL,
|
|
28
|
+
"courseSlugs" TEXT NOT NULL DEFAULT '[]',
|
|
29
|
+
"role" TEXT NOT NULL DEFAULT 'ADMIN',
|
|
30
|
+
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
|
31
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
32
|
+
"updatedAt" DATETIME NOT NULL
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- CreateTable
|
|
36
|
+
CREATE TABLE "Library" (
|
|
37
|
+
"slug" TEXT NOT NULL PRIMARY KEY,
|
|
38
|
+
"title" TEXT NOT NULL,
|
|
39
|
+
"description" TEXT,
|
|
40
|
+
"thumbnail" TEXT,
|
|
41
|
+
"type" TEXT NOT NULL,
|
|
42
|
+
"body" TEXT,
|
|
43
|
+
"courseSlug" TEXT NOT NULL,
|
|
44
|
+
"groupSlug" TEXT NOT NULL,
|
|
45
|
+
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
"allowCommunication" BOOLEAN NOT NULL DEFAULT true,
|
|
47
|
+
"meta" TEXT,
|
|
48
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
49
|
+
"updatedAt" DATETIME NOT NULL
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- CreateTable
|
|
53
|
+
CREATE TABLE "Subscription" (
|
|
54
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
"userId" INTEGER NOT NULL,
|
|
56
|
+
"courseSlug" TEXT NOT NULL,
|
|
57
|
+
"startsAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
58
|
+
"endsAt" DATETIME,
|
|
59
|
+
"amount" INTEGER,
|
|
60
|
+
"cancelledAt" DATETIME,
|
|
61
|
+
CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
-- CreateTable
|
|
65
|
+
CREATE TABLE "Communication" (
|
|
66
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
"userId" INTEGER NOT NULL,
|
|
68
|
+
"librarySlug" TEXT NOT NULL,
|
|
69
|
+
"type" TEXT NOT NULL,
|
|
70
|
+
"meta" TEXT,
|
|
71
|
+
"message" TEXT NOT NULL,
|
|
72
|
+
"authorResponse" TEXT,
|
|
73
|
+
"isPublic" BOOLEAN NOT NULL DEFAULT false,
|
|
74
|
+
"readAt" DATETIME,
|
|
75
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
76
|
+
"updatedAt" DATETIME NOT NULL,
|
|
77
|
+
CONSTRAINT "Communication_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- CreateTable
|
|
81
|
+
CREATE TABLE "Svg" (
|
|
82
|
+
"slug" TEXT NOT NULL PRIMARY KEY,
|
|
83
|
+
"title" TEXT NOT NULL,
|
|
84
|
+
"body" TEXT NOT NULL,
|
|
85
|
+
"tags" TEXT NOT NULL DEFAULT '[]',
|
|
86
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
87
|
+
"updatedAt" DATETIME NOT NULL
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- CreateTable
|
|
91
|
+
CREATE TABLE "Image" (
|
|
92
|
+
"slug" TEXT NOT NULL PRIMARY KEY,
|
|
93
|
+
"title" TEXT NOT NULL,
|
|
94
|
+
"tags" TEXT NOT NULL DEFAULT '[]',
|
|
95
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
96
|
+
"updatedAt" DATETIME NOT NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
-- CreateIndex
|
|
100
|
+
CREATE UNIQUE INDEX "Course_slug_key" ON "Course"("slug");
|
|
101
|
+
|
|
102
|
+
-- CreateIndex
|
|
103
|
+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
104
|
+
|
|
105
|
+
-- CreateIndex
|
|
106
|
+
CREATE UNIQUE INDEX "Admin_email_key" ON "Admin"("email");
|
|
107
|
+
|
|
108
|
+
-- CreateIndex
|
|
109
|
+
CREATE INDEX "Subscription_userId_idx" ON "Subscription"("userId");
|
|
110
|
+
|
|
111
|
+
-- CreateIndex
|
|
112
|
+
CREATE INDEX "Subscription_courseSlug_idx" ON "Subscription"("courseSlug");
|
|
113
|
+
|
|
114
|
+
-- CreateIndex
|
|
115
|
+
CREATE INDEX "Communication_userId_idx" ON "Communication"("userId");
|
|
116
|
+
|
|
117
|
+
-- CreateIndex
|
|
118
|
+
CREATE INDEX "Communication_librarySlug_idx" ON "Communication"("librarySlug");
|
package/prisma/schema.prisma
CHANGED
|
@@ -118,4 +118,31 @@ model Svg {
|
|
|
118
118
|
|
|
119
119
|
updatedAt DateTime @updatedAt
|
|
120
120
|
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
model Image {
|
|
124
|
+
|
|
125
|
+
slug String @id
|
|
126
|
+
|
|
127
|
+
title String
|
|
128
|
+
|
|
129
|
+
tags String @default("[]")
|
|
130
|
+
|
|
131
|
+
createdAt DateTime @default(now())
|
|
132
|
+
|
|
133
|
+
updatedAt DateTime @updatedAt
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
model Audio {
|
|
137
|
+
|
|
138
|
+
slug String @id
|
|
139
|
+
|
|
140
|
+
title String
|
|
141
|
+
|
|
142
|
+
tags String @default("[]")
|
|
143
|
+
|
|
144
|
+
createdAt DateTime @default(now())
|
|
145
|
+
|
|
146
|
+
updatedAt DateTime @updatedAt
|
|
147
|
+
|
|
121
148
|
}
|
package/readme.md
CHANGED
|
@@ -1,116 +1,97 @@
|
|
|
1
|
+
## Taleem Kernel API
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
The **serverKernel** is the product. It contains all business logic and data access. HTTP is only an adapter.
|
|
3
|
+
```js
|
|
4
|
+
import kernel from "taleem-kernel";
|
|
5
|
+
```
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
`taleem-kernel` is the shared DB/domain layer for Taleem applications. It provides Prisma/SQLite access and Taleem-specific modules.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
* Validation
|
|
12
|
-
* Business rules
|
|
13
|
-
* Authentication
|
|
14
|
-
* Authorization
|
|
15
|
-
* Domain errors
|
|
9
|
+
### Core
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
```js
|
|
12
|
+
kernel.db // Prisma client
|
|
13
|
+
kernel.config // configuration
|
|
14
|
+
kernel.auth // authentication/JWT
|
|
15
|
+
kernel.communicationPolicy
|
|
16
|
+
kernel.shutdown()
|
|
17
|
+
```
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
### Modules
|
|
20
|
+
|
|
21
|
+
```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
|
|
31
|
+
```
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
Each module provides simple DB/domain operations such as:
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
```js
|
|
36
|
+
list()
|
|
37
|
+
get(...)
|
|
38
|
+
create(data)
|
|
39
|
+
update(id, data)
|
|
40
|
+
delete(id)
|
|
41
|
+
```
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
with additional module-specific methods such as:
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
```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()
|
|
52
|
+
```
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
### Schema
|
|
40
55
|
|
|
41
|
-
|
|
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.
|
|
42
57
|
|
|
43
|
-
|
|
58
|
+
```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)
|
|
64
|
+
```
|
|
44
65
|
|
|
45
|
-
|
|
46
|
-
* Loading user
|
|
47
|
-
* Checking permissions
|
|
48
|
-
* Checking subscriptions
|
|
49
|
-
* Returning authenticated identity
|
|
66
|
+
`kernel.shutdown()` disconnects Prisma when the application exits.
|
|
50
67
|
|
|
51
|
-
|
|
68
|
+
## Schema Management
|
|
52
69
|
|
|
53
|
-
|
|
70
|
+
The kernel provides two CLI commands for maintaining schema compatibility:
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
```bash
|
|
73
|
+
npx taleem-kernel schema-check
|
|
74
|
+
npx taleem-kernel schema-update
|
|
75
|
+
```
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
* validates authentication (if required)
|
|
59
|
-
* enforces authorization
|
|
60
|
-
* throws explicit errors
|
|
61
|
-
* never silently fails
|
|
62
|
-
* never returns ambiguous `null`
|
|
77
|
+
`schema-check` compares the application's `prisma/schema.prisma` with the canonical schema shipped with `taleem-kernel`.
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
`schema-update` copies the kernel's canonical schema into the application's `prisma/schema.prisma`.
|
|
65
80
|
|
|
66
|
-
|
|
81
|
+
After updating the schema, run the application's normal Prisma migration/generation process as required.
|
|
67
82
|
|
|
68
|
-
|
|
83
|
+
## Server Dependencies
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
2. Why?
|
|
72
|
-
3. Which method?
|
|
73
|
-
4. Which object/user/resource?
|
|
74
|
-
5. What should the caller check?
|
|
85
|
+
`taleem-server` should use `taleem-kernel` for all Taleem database access.
|
|
75
86
|
|
|
76
|
-
|
|
87
|
+
The server provides the HTTP/API layer on top of the kernel:
|
|
77
88
|
|
|
78
89
|
```text
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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"
|
|
84
95
|
```
|
|
85
96
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Every feature must be explainable as a story.
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
HTTP
|
|
92
|
-
↓
|
|
93
|
-
Extract token
|
|
94
|
-
↓
|
|
95
|
-
Kernel
|
|
96
|
-
↓
|
|
97
|
-
Authenticate
|
|
98
|
-
↓
|
|
99
|
-
Authorize
|
|
100
|
-
↓
|
|
101
|
-
Business logic
|
|
102
|
-
↓
|
|
103
|
-
Return result
|
|
104
|
-
↓
|
|
105
|
-
HTTP formats response
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
No business logic is allowed before entering the kernel.
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
I would add one final principle because it will save you from years of debugging:
|
|
113
|
-
|
|
114
|
-
> **The kernel trusts nothing and explains everything.**
|
|
115
|
-
|
|
116
|
-
That one sentence should be visible at the top of the `serverKernel` folder. It captures the philosophy you've been moving toward: every input is validated, every decision is explicit, and every failure tells you exactly why it happened.
|
|
97
|
+
The server should **not maintain its own Taleem Prisma models or DB modules**.
|
package/src/ServerKernel.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import { PrismaClient } from "@prisma/client";
|
|
1
|
+
// src/serverKernel/ServerKernel.js
|
|
3
2
|
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
4
|
import Config from "./Config.js";
|
|
5
5
|
import Auth from "./Auth.js";
|
|
6
|
-
import Logger from "./Logger.js";
|
|
7
6
|
import CommunicationPolicy from "./CommunicationPolicy.js";
|
|
8
7
|
import User from "./modules/User.js";
|
|
9
8
|
import Admin from "./modules/Admin.js";
|
|
@@ -16,122 +15,25 @@ import Audio from "./modules/Audio.js";
|
|
|
16
15
|
import Svg from "./modules/Svg.js";
|
|
17
16
|
|
|
18
17
|
class ServerKernel {
|
|
19
|
-
|
|
20
18
|
constructor() {
|
|
21
|
-
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.config = this.initialize("Config", () => new Config());
|
|
35
|
-
|
|
36
|
-
this.db = this.initialize("Prisma", () => new PrismaClient());
|
|
37
|
-
|
|
38
|
-
this.auth = this.initialize("Auth", () => new Auth(this));
|
|
39
|
-
|
|
40
|
-
this.communicationPolicy =
|
|
41
|
-
this.initialize(
|
|
42
|
-
"CommunicationPolicy",
|
|
43
|
-
() => new CommunicationPolicy(this)
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
// --------------------------------------------------
|
|
47
|
-
// Modules
|
|
48
|
-
// --------------------------------------------------
|
|
49
|
-
this.user = this.initialize("User", () => new User(this));
|
|
50
|
-
this.admin = this.initialize("Admin", () => new Admin(this));
|
|
51
|
-
this.library = this.initialize("Library", () => new Library(this));
|
|
52
|
-
this.course = this.initialize("Course", () => new Course(this));
|
|
53
|
-
this.image = this.initialize("Image", () => new Image(this));
|
|
54
|
-
this.audio = this.initialize("Audio", () => new Audio(this));
|
|
55
|
-
this.svg = this.initialize("Svg", () => new Svg(this));
|
|
56
|
-
this.communication = this.initialize("Communication", () => new Communication(this));
|
|
57
|
-
this.subscription = this.initialize("Subscription", () => new Subscription(this));
|
|
58
|
-
|
|
59
|
-
this.logger.info("Server Kernel started successfully.");
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
|
|
64
|
-
this.logger.error(error.message);
|
|
65
|
-
|
|
66
|
-
throw error;
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
initialize(name, factory) {
|
|
73
|
-
|
|
74
|
-
this.logger.info(`Initializing ${name}...`);
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
|
|
78
|
-
const instance = factory();
|
|
79
|
-
|
|
80
|
-
this.logger.info(`${name} initialized.`);
|
|
81
|
-
|
|
82
|
-
return instance;
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
|
|
87
|
-
throw new Error(
|
|
88
|
-
[
|
|
89
|
-
"",
|
|
90
|
-
"========================================",
|
|
91
|
-
"SERVER KERNEL INITIALIZATION FAILED",
|
|
92
|
-
"----------------------------------------",
|
|
93
|
-
`Component : ${name}`,
|
|
94
|
-
`Reason : ${error.message}`,
|
|
95
|
-
"",
|
|
96
|
-
"Server Kernel startup aborted.",
|
|
97
|
-
"========================================"
|
|
98
|
-
].join("\n")
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
19
|
+
this.config = new Config();
|
|
20
|
+
this.db = new PrismaClient();
|
|
21
|
+
this.auth = new Auth(this);
|
|
22
|
+
this.communicationPolicy = new CommunicationPolicy(this);
|
|
23
|
+
this.user = new User(this);
|
|
24
|
+
this.admin = new Admin(this);
|
|
25
|
+
this.library = new Library(this);
|
|
26
|
+
this.course = new Course(this);
|
|
27
|
+
this.image = new Image(this);
|
|
28
|
+
this.audio = new Audio(this);
|
|
29
|
+
this.svg = new Svg(this);
|
|
30
|
+
this.communication = new Communication(this);
|
|
31
|
+
this.subscription = new Subscription(this);
|
|
103
32
|
}
|
|
104
33
|
|
|
105
34
|
async shutdown() {
|
|
106
|
-
|
|
107
|
-
this.logger.info("Shutting down Server Kernel...");
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
|
|
111
|
-
await this.db.$disconnect();
|
|
112
|
-
|
|
113
|
-
this.logger.info("Database disconnected.");
|
|
114
|
-
|
|
115
|
-
this.logger.info("Server Kernel shutdown complete.");
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
|
|
120
|
-
throw new Error(
|
|
121
|
-
[
|
|
122
|
-
"",
|
|
123
|
-
"========================================",
|
|
124
|
-
"SERVER KERNEL SHUTDOWN FAILED",
|
|
125
|
-
"----------------------------------------",
|
|
126
|
-
`Reason : ${error.message}`,
|
|
127
|
-
"========================================"
|
|
128
|
-
].join("\n")
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
35
|
+
await this.db.$disconnect();
|
|
133
36
|
}
|
|
134
|
-
|
|
135
37
|
}
|
|
136
38
|
|
|
137
39
|
const kernel = new ServerKernel();
|
package/src/modules/Admin.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// src/serverKernel/modules/Admin.js
|
|
2
|
+
|
|
1
3
|
import bcrypt from "bcrypt";
|
|
2
4
|
|
|
3
5
|
export default class Admin {
|
|
@@ -10,15 +12,11 @@ export default class Admin {
|
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
async get(email) {
|
|
13
|
-
return this.kernel.db.admin.findUnique({
|
|
14
|
-
where: { email }
|
|
15
|
-
});
|
|
15
|
+
return this.kernel.db.admin.findUnique({ where: { email } });
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async login(email, password) {
|
|
19
|
-
const admin = await this.
|
|
20
|
-
where: { email }
|
|
21
|
-
});
|
|
19
|
+
const admin = await this.get(email);
|
|
22
20
|
if (!admin) throw new Error(`Admin.login(): Admin '${email}' not found.`);
|
|
23
21
|
if (!admin.isActive) throw new Error(`Admin.login(): Admin '${email}' is inactive.`);
|
|
24
22
|
|
|
@@ -29,29 +27,25 @@ export default class Admin {
|
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
async create(data) {
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const createData = { ...data };
|
|
31
|
+
if (createData.password) createData.password = await bcrypt.hash(createData.password, 10);
|
|
32
|
+
return this.kernel.db.admin.create({ data: createData });
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
async update(email, data) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
data
|
|
41
|
-
});
|
|
36
|
+
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 });
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
async delete(email) {
|
|
45
|
-
return this.kernel.db.admin.delete({
|
|
46
|
-
where: { email }
|
|
47
|
-
});
|
|
42
|
+
return this.kernel.db.admin.delete({ where: { email } });
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
async isAdmin(email, courseSlug) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
46
|
+
const admin = await this.get(email);
|
|
47
|
+
if (!admin) return false;
|
|
48
|
+
const courseSlugs = JSON.parse(admin.courseSlugs || "[]");
|
|
49
|
+
return courseSlugs.includes(courseSlug);
|
|
50
|
+
}
|
|
57
51
|
}
|
package/src/modules/Audio.js
CHANGED
|
@@ -1,49 +1,27 @@
|
|
|
1
1
|
// src/serverKernel/modules/Audio.js
|
|
2
2
|
|
|
3
3
|
export default class Audio {
|
|
4
|
-
|
|
5
|
-
constructor(kernel) {
|
|
6
|
-
this.kernel = kernel;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// --------------------------------------------------
|
|
10
|
-
// Queries
|
|
11
|
-
// --------------------------------------------------
|
|
4
|
+
constructor(kernel) { this.kernel = kernel; }
|
|
12
5
|
|
|
13
6
|
async list() {
|
|
14
|
-
|
|
15
7
|
return this.kernel.db.audio.findMany({
|
|
16
|
-
|
|
17
|
-
orderBy: {
|
|
18
|
-
createdAt: "desc"
|
|
19
|
-
}
|
|
20
|
-
|
|
8
|
+
orderBy: { createdAt: "desc" }
|
|
21
9
|
});
|
|
22
|
-
|
|
23
10
|
}
|
|
24
11
|
|
|
25
|
-
async get(
|
|
26
|
-
|
|
27
|
-
return this.kernel.db.audio.findUnique({
|
|
28
|
-
|
|
29
|
-
where: { id }
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
|
|
12
|
+
async get(slug) {
|
|
13
|
+
return this.kernel.db.audio.findUnique({ where: { slug } });
|
|
33
14
|
}
|
|
34
15
|
|
|
35
|
-
// --------------------------------------------------
|
|
36
|
-
// Create
|
|
37
|
-
// --------------------------------------------------
|
|
38
|
-
|
|
39
16
|
async create(data) {
|
|
17
|
+
return this.kernel.db.audio.create({ data });
|
|
18
|
+
}
|
|
40
19
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
data
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
|
|
20
|
+
async update(slug, data) {
|
|
21
|
+
return this.kernel.db.audio.update({ where: { slug }, data });
|
|
47
22
|
}
|
|
48
23
|
|
|
24
|
+
async delete(slug) {
|
|
25
|
+
return this.kernel.db.audio.delete({ where: { slug } });
|
|
26
|
+
}
|
|
49
27
|
}
|
package/src/modules/Image.js
CHANGED
|
@@ -1,48 +1,27 @@
|
|
|
1
|
+
// src/serverKernel/modules/Image.js
|
|
1
2
|
|
|
2
3
|
export default class Image {
|
|
3
|
-
|
|
4
|
-
constructor(kernel) {
|
|
5
|
-
this.kernel = kernel;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// --------------------------------------------------
|
|
9
|
-
// Queries
|
|
10
|
-
// --------------------------------------------------
|
|
4
|
+
constructor(kernel) { this.kernel = kernel; }
|
|
11
5
|
|
|
12
6
|
async list() {
|
|
13
|
-
|
|
14
7
|
return this.kernel.db.image.findMany({
|
|
15
|
-
|
|
16
|
-
orderBy: {
|
|
17
|
-
createdAt: "desc"
|
|
18
|
-
}
|
|
19
|
-
|
|
8
|
+
orderBy: { createdAt: "desc" }
|
|
20
9
|
});
|
|
21
|
-
|
|
22
10
|
}
|
|
23
11
|
|
|
24
|
-
async get(
|
|
25
|
-
|
|
26
|
-
return this.kernel.db.image.findUnique({
|
|
27
|
-
|
|
28
|
-
where: { id }
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
|
|
12
|
+
async get(slug) {
|
|
13
|
+
return this.kernel.db.image.findUnique({ where: { slug } });
|
|
32
14
|
}
|
|
33
15
|
|
|
34
|
-
// --------------------------------------------------
|
|
35
|
-
// Create
|
|
36
|
-
// --------------------------------------------------
|
|
37
|
-
|
|
38
16
|
async create(data) {
|
|
17
|
+
return this.kernel.db.image.create({ data });
|
|
18
|
+
}
|
|
39
19
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
data
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
|
|
20
|
+
async update(slug, data) {
|
|
21
|
+
return this.kernel.db.image.update({ where: { slug }, data });
|
|
46
22
|
}
|
|
47
23
|
|
|
24
|
+
async delete(slug) {
|
|
25
|
+
return this.kernel.db.image.delete({ where: { slug } });
|
|
26
|
+
}
|
|
48
27
|
}
|
package/src/modules/Svg.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
// src/serverKernel/modules/Svg.js
|
|
2
|
+
|
|
1
3
|
export default class Svg {
|
|
4
|
+
constructor(kernel) { this.kernel = kernel; }
|
|
5
|
+
|
|
6
|
+
async list() {
|
|
7
|
+
return this.kernel.db.svg.findMany({
|
|
8
|
+
orderBy: { title: "asc" }
|
|
9
|
+
});
|
|
10
|
+
}
|
|
2
11
|
|
|
3
|
-
|
|
4
|
-
this.kernel
|
|
12
|
+
async get(slug) {
|
|
13
|
+
return this.kernel.db.svg.findUnique({ where: { slug } });
|
|
5
14
|
}
|
|
6
15
|
|
|
7
16
|
async create(data) {
|
|
8
|
-
|
|
9
17
|
return this.kernel.db.svg.create({
|
|
10
18
|
data: {
|
|
11
19
|
slug: data.slug,
|
|
@@ -14,29 +22,9 @@ export default class Svg {
|
|
|
14
22
|
tags: data.tags ?? "[]"
|
|
15
23
|
}
|
|
16
24
|
});
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async get(slug) {
|
|
21
|
-
|
|
22
|
-
return this.kernel.db.svg.findUnique({
|
|
23
|
-
where: { slug }
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async list() {
|
|
29
|
-
|
|
30
|
-
return this.kernel.db.svg.findMany({
|
|
31
|
-
orderBy: {
|
|
32
|
-
title: "asc"
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
async update(slug, data) {
|
|
39
|
-
|
|
40
28
|
return this.kernel.db.svg.update({
|
|
41
29
|
where: { slug },
|
|
42
30
|
data: {
|
|
@@ -45,15 +33,9 @@ export default class Svg {
|
|
|
45
33
|
tags: data.tags
|
|
46
34
|
}
|
|
47
35
|
});
|
|
48
|
-
|
|
49
36
|
}
|
|
50
37
|
|
|
51
38
|
async delete(slug) {
|
|
52
|
-
|
|
53
|
-
return this.kernel.db.svg.delete({
|
|
54
|
-
where: { slug }
|
|
55
|
-
});
|
|
56
|
-
|
|
39
|
+
return this.kernel.db.svg.delete({ where: { slug } });
|
|
57
40
|
}
|
|
58
|
-
|
|
59
41
|
}
|
package/src/modules/User.js
CHANGED
|
@@ -3,115 +3,56 @@
|
|
|
3
3
|
import bcrypt from "bcrypt";
|
|
4
4
|
|
|
5
5
|
export default class User {
|
|
6
|
-
|
|
7
6
|
constructor(kernel) {
|
|
8
7
|
this.kernel = kernel;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
// --------------------------------------------------
|
|
12
10
|
// Queries
|
|
13
|
-
// --------------------------------------------------
|
|
14
11
|
|
|
15
12
|
async list() {
|
|
16
|
-
|
|
17
13
|
return this.kernel.db.user.findMany();
|
|
18
|
-
|
|
19
14
|
}
|
|
20
15
|
|
|
21
16
|
async get(id) {
|
|
22
|
-
|
|
23
|
-
return this.kernel.db.user.findUnique({
|
|
24
|
-
where: { id }
|
|
25
|
-
});
|
|
26
|
-
|
|
17
|
+
return this.kernel.db.user.findUnique({ where: { id } });
|
|
27
18
|
}
|
|
28
19
|
|
|
29
20
|
async getByEmail(email) {
|
|
30
|
-
|
|
31
|
-
return this.kernel.db.user.findUnique({
|
|
32
|
-
where: { email }
|
|
33
|
-
});
|
|
34
|
-
|
|
21
|
+
return this.kernel.db.user.findUnique({ where: { email } });
|
|
35
22
|
}
|
|
36
23
|
|
|
37
24
|
async emailToId(email) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!user) {
|
|
42
|
-
throw new Error(`User '${email}' not found.`);
|
|
25
|
+
const user = await this.getByEmail(email);
|
|
26
|
+
if (!user) throw new Error(`User '${email}' not found.`);
|
|
27
|
+
return user.id;
|
|
43
28
|
}
|
|
44
29
|
|
|
45
|
-
return user.id;
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
// --------------------------------------------------
|
|
49
30
|
// Authentication
|
|
50
|
-
// --------------------------------------------------
|
|
51
31
|
|
|
52
32
|
async register(data) {
|
|
53
|
-
|
|
54
33
|
const password = await bcrypt.hash(data.password, 10);
|
|
55
|
-
|
|
56
|
-
return this.kernel.db.user.create({
|
|
57
|
-
|
|
58
|
-
data: {
|
|
59
|
-
...data,
|
|
60
|
-
password
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
|
|
34
|
+
return this.kernel.db.user.create({ data: { ...data, password } });
|
|
65
35
|
}
|
|
66
36
|
|
|
67
37
|
async login(email, password) {
|
|
68
|
-
|
|
69
38
|
const user = await this.getByEmail(email);
|
|
70
|
-
|
|
71
|
-
if (!user) {
|
|
72
|
-
throw new Error(`User.login(): User '${email}' not found.`);
|
|
73
|
-
}
|
|
39
|
+
if (!user) throw new Error(`User.login(): User '${email}' not found.`);
|
|
74
40
|
|
|
75
41
|
const ok = await bcrypt.compare(password, user.password);
|
|
76
|
-
|
|
77
|
-
if (!ok) {
|
|
78
|
-
throw new Error(`User.login(): Invalid password.`);
|
|
79
|
-
}
|
|
42
|
+
if (!ok) throw new Error(`User.login(): Invalid password.`);
|
|
80
43
|
|
|
81
44
|
return this.kernel.auth.createUserToken(user);
|
|
82
|
-
|
|
83
45
|
}
|
|
84
46
|
|
|
85
|
-
// --------------------------------------------------
|
|
86
47
|
// CRUD
|
|
87
|
-
// --------------------------------------------------
|
|
88
48
|
|
|
89
49
|
async update(id, data) {
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
data.password = await bcrypt.hash(data.password, 10);
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return this.kernel.db.user.update({
|
|
98
|
-
|
|
99
|
-
where: { id },
|
|
100
|
-
|
|
101
|
-
data
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
|
|
50
|
+
const updateData = { ...data };
|
|
51
|
+
if (updateData.password) updateData.password = await bcrypt.hash(updateData.password, 10);
|
|
52
|
+
return this.kernel.db.user.update({ where: { id }, data: updateData });
|
|
105
53
|
}
|
|
106
54
|
|
|
107
55
|
async delete(id) {
|
|
108
|
-
|
|
109
|
-
return this.kernel.db.user.delete({
|
|
110
|
-
|
|
111
|
-
where: { id }
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
|
|
56
|
+
return this.kernel.db.user.delete({ where: { id } });
|
|
115
57
|
}
|
|
116
|
-
|
|
117
58
|
}
|
package/.env
DELETED
package/src/Logger.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// src/serverKernel/Logger.js
|
|
2
|
-
|
|
3
|
-
export default class Logger {
|
|
4
|
-
|
|
5
|
-
info(...args) {
|
|
6
|
-
|
|
7
|
-
console.log("[INFO]", ...args);
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
warn(...args) {
|
|
12
|
-
|
|
13
|
-
console.warn("[WARN]", ...args);
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
error(...args) {
|
|
18
|
-
|
|
19
|
-
console.error("[ERROR]", ...args);
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
debug(...args) {
|
|
24
|
-
|
|
25
|
-
if (process.env.NODE_ENV !== "production") {
|
|
26
|
-
|
|
27
|
-
console.log("[DEBUG]", ...args);
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
}
|
package/tests/admin.test.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
-
import kernel from "../src/ServerKernel.js";
|
|
3
|
-
|
|
4
|
-
const email = "bilal@taleem.help";
|
|
5
|
-
|
|
6
|
-
describe("Admin", () => {
|
|
7
|
-
it("gets admin by email", async () => {
|
|
8
|
-
const admin = await kernel.admin.get(email);
|
|
9
|
-
|
|
10
|
-
expect(admin).toBeTruthy();
|
|
11
|
-
expect(admin.email).toBe(email);
|
|
12
|
-
expect(admin.isActive).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("lists active admins", async () => {
|
|
16
|
-
const admins = await kernel.admin.list({ isActive: true });
|
|
17
|
-
|
|
18
|
-
expect(Array.isArray(admins)).toBe(true);
|
|
19
|
-
expect(admins.some(admin => admin.email === email)).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("checks course authorization", async () => {
|
|
23
|
-
expect(
|
|
24
|
-
await kernel.admin.isAdmin(email, "fbise9math")
|
|
25
|
-
).toBe(true);
|
|
26
|
-
|
|
27
|
-
expect(
|
|
28
|
-
await kernel.admin.isAdmin(email, "blog")
|
|
29
|
-
).toBe(true);
|
|
30
|
-
|
|
31
|
-
expect(
|
|
32
|
-
await kernel.admin.isAdmin(email, "fbise9mathQuickRef")
|
|
33
|
-
).toBe(true);
|
|
34
|
-
|
|
35
|
-
expect(
|
|
36
|
-
await kernel.admin.isAdmin(email, "does-not-exist")
|
|
37
|
-
).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns false for an unknown admin", async () => {
|
|
41
|
-
expect(
|
|
42
|
-
await kernel.admin.isAdmin(
|
|
43
|
-
"unknown@example.com",
|
|
44
|
-
"fbise9math"
|
|
45
|
-
)
|
|
46
|
-
).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
afterAll(async () => {
|
|
50
|
-
await kernel.shutdown();
|
|
51
|
-
});
|
|
52
|
-
});
|
package/tests/course.test.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
-
import kernel from "../src/ServerKernel.js";
|
|
3
|
-
|
|
4
|
-
describe("Course", () => {
|
|
5
|
-
let testSlug;
|
|
6
|
-
|
|
7
|
-
it("lists courses", async () => {
|
|
8
|
-
const courses = await kernel.course.list();
|
|
9
|
-
expect(Array.isArray(courses)).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("gets a course by slug", async () => {
|
|
13
|
-
const courses = await kernel.course.list();
|
|
14
|
-
expect(courses.length).toBeGreaterThan(0);
|
|
15
|
-
|
|
16
|
-
const course = await kernel.course.get(courses[0].slug);
|
|
17
|
-
|
|
18
|
-
expect(course).toBeTruthy();
|
|
19
|
-
expect(course.slug).toBe(courses[0].slug);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("filters courses by access", async () => {
|
|
23
|
-
const courses = await kernel.course.list({ access: "OPEN" });
|
|
24
|
-
|
|
25
|
-
expect(Array.isArray(courses)).toBe(true);
|
|
26
|
-
|
|
27
|
-
for (const course of courses) {
|
|
28
|
-
expect(course.access).toBe("OPEN");
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("creates, updates and deletes a course", async () => {
|
|
33
|
-
testSlug = "__kernel-test-course__";
|
|
34
|
-
|
|
35
|
-
await kernel.course.create({
|
|
36
|
-
slug: testSlug,
|
|
37
|
-
title: "Kernel Test Course",
|
|
38
|
-
description: "Temporary test course",
|
|
39
|
-
access: "OPEN"
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
let course = await kernel.course.get(testSlug);
|
|
43
|
-
expect(course).toBeTruthy();
|
|
44
|
-
expect(course.title).toBe("Kernel Test Course");
|
|
45
|
-
|
|
46
|
-
await kernel.course.update(testSlug, {
|
|
47
|
-
title: "Updated Kernel Test Course"
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
course = await kernel.course.get(testSlug);
|
|
51
|
-
expect(course.title).toBe("Updated Kernel Test Course");
|
|
52
|
-
|
|
53
|
-
await kernel.course.delete(testSlug);
|
|
54
|
-
|
|
55
|
-
course = await kernel.course.get(testSlug);
|
|
56
|
-
expect(course).toBeNull();
|
|
57
|
-
|
|
58
|
-
testSlug = null;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
afterAll(async () => {
|
|
62
|
-
if (testSlug) {
|
|
63
|
-
await kernel.course.delete(testSlug).catch(() => {});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
await kernel.shutdown();
|
|
67
|
-
});
|
|
68
|
-
});
|
package/tests/kernel.test.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
-
import kernel from "../src/ServerKernel.js";
|
|
3
|
-
|
|
4
|
-
describe("Taleem Kernel", () => {
|
|
5
|
-
it("starts and connects to Prisma", async () => {
|
|
6
|
-
await expect(kernel.db.$queryRaw`SELECT 1`).resolves.toBeDefined();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
afterAll(async () => {
|
|
10
|
-
await kernel.shutdown();
|
|
11
|
-
});
|
|
12
|
-
});
|
package/tests/library.test.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
-
import kernel from "../src/ServerKernel.js";
|
|
3
|
-
|
|
4
|
-
describe("Library", () => {
|
|
5
|
-
it("lists library content", async () => {
|
|
6
|
-
const items = await kernel.library.list();
|
|
7
|
-
expect(Array.isArray(items)).toBe(true);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("gets a library item by slug", async () => {
|
|
11
|
-
const items = await kernel.library.list();
|
|
12
|
-
expect(items.length).toBeGreaterThan(0);
|
|
13
|
-
|
|
14
|
-
const item = await kernel.library.get(items[0].slug);
|
|
15
|
-
|
|
16
|
-
expect(item).toBeTruthy();
|
|
17
|
-
expect(item.slug).toBe(items[0].slug);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterAll(async () => {
|
|
21
|
-
await kernel.shutdown();
|
|
22
|
-
});
|
|
23
|
-
});
|