taleem-kernel 1.0.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.
@@ -0,0 +1,116 @@
1
+ export default class Communication {
2
+
3
+ constructor(kernel) {
4
+ this.kernel = kernel;
5
+ }
6
+
7
+ async list(filters = {}) {
8
+ const where = {};
9
+
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
+ };
19
+ }
20
+
21
+ if (filters.librarySlug) where.librarySlug = filters.librarySlug;
22
+ if (filters.userId) where.userId = filters.userId;
23
+
24
+ if (filters.unanswered) {
25
+ where.OR = [
26
+ { authorResponse: null },
27
+ { authorResponse: "" }
28
+ ];
29
+ }
30
+
31
+ return this.kernel.db.communication.findMany({
32
+ where,
33
+ include: { user: true },
34
+ orderBy: { createdAt: "desc" }
35
+ });
36
+ }
37
+
38
+ async get(id) {
39
+ const item = await this.kernel.db.communication.findUnique({
40
+ where: { id },
41
+ include: { user: true }
42
+ });
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
+ }
60
+
61
+ async create(data) {
62
+ return this.kernel.db.communication.create({ data });
63
+ }
64
+
65
+ async update(id, data) {
66
+ return this.kernel.db.communication.update({
67
+ where: { id },
68
+ data
69
+ });
70
+ }
71
+
72
+ async delete(id) {
73
+ return this.kernel.db.communication.delete({
74
+ where: { id }
75
+ });
76
+ }
77
+
78
+ 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
+ }));
103
+ }
104
+
105
+ async countUserOpenQuestions(userId) {
106
+ return this.kernel.db.communication.count({
107
+ where: {
108
+ userId,
109
+ OR: [
110
+ { authorResponse: null },
111
+ { authorResponse: "" }
112
+ ]
113
+ }
114
+ });
115
+ }
116
+ }
@@ -0,0 +1,26 @@
1
+ export default class Course {
2
+ constructor(kernel) { this.kernel = kernel; }
3
+
4
+ async list(filters = {}) {
5
+ const where = {};
6
+ if (filters.access) where.access = filters.access;
7
+ return this.kernel.db.course.findMany({ where });
8
+ }
9
+
10
+ async get(slug) {
11
+ return this.kernel.db.course.findUnique({ where: { slug } });
12
+ }
13
+
14
+ async create(data) {
15
+ return this.kernel.db.course.create({ data });
16
+ }
17
+
18
+ async update(slug, data) {
19
+ return this.kernel.db.course.update({ where: { slug }, data });
20
+ }
21
+
22
+ async delete(slug) {
23
+ return this.kernel.db.course.delete({ where: { slug } });
24
+ }
25
+
26
+ }
@@ -0,0 +1,48 @@
1
+
2
+ export default class Image {
3
+
4
+ constructor(kernel) {
5
+ this.kernel = kernel;
6
+ }
7
+
8
+ // --------------------------------------------------
9
+ // Queries
10
+ // --------------------------------------------------
11
+
12
+ async list() {
13
+
14
+ return this.kernel.db.image.findMany({
15
+
16
+ orderBy: {
17
+ createdAt: "desc"
18
+ }
19
+
20
+ });
21
+
22
+ }
23
+
24
+ async get(id) {
25
+
26
+ return this.kernel.db.image.findUnique({
27
+
28
+ where: { id }
29
+
30
+ });
31
+
32
+ }
33
+
34
+ // --------------------------------------------------
35
+ // Create
36
+ // --------------------------------------------------
37
+
38
+ async create(data) {
39
+
40
+ return this.kernel.db.image.create({
41
+
42
+ data
43
+
44
+ });
45
+
46
+ }
47
+
48
+ }
@@ -0,0 +1,59 @@
1
+ export default class Library {
2
+ constructor(kernel) { this.kernel = kernel; }
3
+
4
+ async list(filters = {}) {
5
+ const where = {};
6
+ if (filters.type) where.type = filters.type;
7
+ if (filters.courseSlug) where.courseSlug = filters.courseSlug;
8
+ if (filters.groupSlug) where.groupSlug = filters.groupSlug;
9
+
10
+ return this.kernel.db.library.findMany({
11
+ where,
12
+ select: {
13
+ slug: true,
14
+ title: true,
15
+ description: true,
16
+ thumbnail: true,
17
+ type: true,
18
+ courseSlug: true,
19
+ groupSlug: true,
20
+ sortOrder: true,
21
+ allowCommunication: true,
22
+ createdAt: true,
23
+ updatedAt: true
24
+ },
25
+ orderBy: { sortOrder: "asc" }
26
+ });
27
+ }
28
+
29
+ async listByCourse(courseSlug) {
30
+ return this.list({ courseSlug });
31
+ }
32
+
33
+ async listByGroup(courseSlug, groupSlug) {
34
+ return this.list({ courseSlug, groupSlug });
35
+ }
36
+
37
+ async get(slug) {
38
+ return this.kernel.db.library.findUnique({
39
+ where: { slug }
40
+ });
41
+ }
42
+
43
+ async create(data) {
44
+ return this.kernel.db.library.create({ data });
45
+ }
46
+
47
+ async update(slug, data) {
48
+ return this.kernel.db.library.update({
49
+ where: { slug },
50
+ data
51
+ });
52
+ }
53
+
54
+ async delete(slug) {
55
+ return this.kernel.db.library.delete({
56
+ where: { slug }
57
+ });
58
+ }
59
+ }
@@ -0,0 +1,97 @@
1
+ // src/serverKernel/modules/Subscription.js
2
+
3
+ export default class Subscription {
4
+
5
+ constructor(kernel) {
6
+ this.kernel = kernel;
7
+ }
8
+
9
+ // --------------------------------------------------
10
+ // Queries
11
+ // --------------------------------------------------
12
+
13
+ async list(filters = {}) {
14
+
15
+ const where = {};
16
+
17
+ if (filters.userId) {
18
+ where.userId = filters.userId;
19
+ }
20
+
21
+ if (filters.courseId) {
22
+ where.courseId = filters.courseId;
23
+ }
24
+
25
+ return this.kernel.db.subscription.findMany({
26
+ where
27
+ });
28
+
29
+ }
30
+
31
+ async get(id) {
32
+
33
+ return this.kernel.db.subscription.findUnique({
34
+ where: { id }
35
+ });
36
+
37
+ }
38
+
39
+ // --------------------------------------------------
40
+ // CRUD
41
+ // --------------------------------------------------
42
+
43
+ async create(data) {
44
+
45
+ return this.kernel.db.subscription.create({
46
+ data
47
+ });
48
+
49
+ }
50
+
51
+ async update(id, data) {
52
+
53
+ return this.kernel.db.subscription.update({
54
+ where: { id },
55
+ data
56
+ });
57
+
58
+ }
59
+
60
+ async delete(id) {
61
+
62
+ return this.kernel.db.subscription.delete({
63
+ where: { id }
64
+ });
65
+
66
+ }
67
+
68
+ // --------------------------------------------------
69
+ // Utilities
70
+ // --------------------------------------------------
71
+
72
+ async authorize(userId, courseId) {
73
+ debugger;
74
+ const now = new Date();
75
+
76
+ const subscription = await this.kernel.db.subscription.findFirst({
77
+
78
+ where: {
79
+ userId,
80
+ courseId,
81
+ startsAt: { lte: now },
82
+ endsAt: { gte: now }
83
+ }
84
+
85
+ });
86
+
87
+ if (!subscription) {
88
+ throw new Error(
89
+ `User "${userId}" does not have an active subscription for course "${courseId}".`
90
+ );
91
+ }
92
+
93
+ return subscription;
94
+
95
+ }
96
+
97
+ }
@@ -0,0 +1,59 @@
1
+ export default class Svg {
2
+
3
+ constructor(kernel) {
4
+ this.kernel = kernel;
5
+ }
6
+
7
+ async create(data) {
8
+
9
+ return this.kernel.db.svg.create({
10
+ data: {
11
+ slug: data.slug,
12
+ title: data.title,
13
+ body: data.body,
14
+ tags: data.tags ?? "[]"
15
+ }
16
+ });
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
+ }
37
+
38
+ async update(slug, data) {
39
+
40
+ return this.kernel.db.svg.update({
41
+ where: { slug },
42
+ data: {
43
+ title: data.title,
44
+ body: data.body,
45
+ tags: data.tags
46
+ }
47
+ });
48
+
49
+ }
50
+
51
+ async delete(slug) {
52
+
53
+ return this.kernel.db.svg.delete({
54
+ where: { slug }
55
+ });
56
+
57
+ }
58
+
59
+ }
@@ -0,0 +1,117 @@
1
+ // src/serverKernel/modules/User.js
2
+
3
+ import bcrypt from "bcrypt";
4
+
5
+ export default class User {
6
+
7
+ constructor(kernel) {
8
+ this.kernel = kernel;
9
+ }
10
+
11
+ // --------------------------------------------------
12
+ // Queries
13
+ // --------------------------------------------------
14
+
15
+ async list() {
16
+
17
+ return this.kernel.db.user.findMany();
18
+
19
+ }
20
+
21
+ async get(id) {
22
+
23
+ return this.kernel.db.user.findUnique({
24
+ where: { id }
25
+ });
26
+
27
+ }
28
+
29
+ async getByEmail(email) {
30
+
31
+ return this.kernel.db.user.findUnique({
32
+ where: { email }
33
+ });
34
+
35
+ }
36
+
37
+ async emailToId(email) {
38
+
39
+ const user = await this.getByEmail(email);
40
+
41
+ if (!user) {
42
+ throw new Error(`User '${email}' not found.`);
43
+ }
44
+
45
+ return user.id;
46
+
47
+ }
48
+ // --------------------------------------------------
49
+ // Authentication
50
+ // --------------------------------------------------
51
+
52
+ async register(data) {
53
+
54
+ 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
+
65
+ }
66
+
67
+ async login(email, password) {
68
+
69
+ const user = await this.getByEmail(email);
70
+
71
+ if (!user) {
72
+ throw new Error(`User.login(): User '${email}' not found.`);
73
+ }
74
+
75
+ const ok = await bcrypt.compare(password, user.password);
76
+
77
+ if (!ok) {
78
+ throw new Error(`User.login(): Invalid password.`);
79
+ }
80
+
81
+ return this.kernel.auth.createUserToken(user);
82
+
83
+ }
84
+
85
+ // --------------------------------------------------
86
+ // CRUD
87
+ // --------------------------------------------------
88
+
89
+ async update(id, data) {
90
+
91
+ if (data.password) {
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
+
105
+ }
106
+
107
+ async delete(id) {
108
+
109
+ return this.kernel.db.user.delete({
110
+
111
+ where: { id }
112
+
113
+ });
114
+
115
+ }
116
+
117
+ }
@@ -0,0 +1,52 @@
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
+ });
@@ -0,0 +1,68 @@
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
+ });
@@ -0,0 +1,12 @@
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
+ });
@@ -0,0 +1,23 @@
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
+ });