taleem-kernel
v1.6.0
Published
Taleem data and business-logic kernel. HTTP is only an adapter.
Downloads
890
Readme
Taleem Kernel — API Reference
v1.6.0
import kernel from "taleem-kernel";Shared DB/domain layer for Taleem apps. Owns the canonical Prisma/SQLite schema. Exposes domain modules — not raw CRUD.
Module Index
| Module | Purpose |
|---|---|
| kernel.user | App user registration/auth |
| kernel.admin | Course-scoped + super admin accounts |
| kernel.course | Course CRUD + access control |
| kernel.group | Structural grouping within a Course |
| kernel.library | Authored content (articles, players, etc.) |
| kernel.communication | Discussion threads on Library items |
| kernel.subscription | User access to a Course |
| kernel.svg / kernel.image / kernel.audio | Independent reusable assets |
| kernel.jwt | Low-level token utility (avoid calling directly) |
| kernel.db | Raw Prisma client (escape hatch) |
| kernel.config | Kernel config |
| kernel.shutdown() | Close DB connection |
Course
└─ Group
└─ Library ─── Communication
Course ─── SubscriptionAll relations onDelete: Restrict — nothing cascades. Detach children before deleting a parent.
User
kernel.user.list()
kernel.user.get(id)
kernel.user.getByEmail(email)
kernel.user.emailToId(email)
kernel.user.register({ email, password })
kernel.user.login(email, password) // → JWT
kernel.user.createToken(user)
kernel.user.authenticate(token) // → User; type: "user" tokens only
kernel.user.update(id, data)
kernel.user.delete(id)Admin
courseSlugs is a plain JSON string field, not a relation — script-managed, unvalidated at the DB level.
kernel.admin.list(filters) // { isActive }
kernel.admin.get(email)
kernel.admin.login(email, password) // → JWT
kernel.admin.createToken(admin)
kernel.admin.authenticate(token) // → Admin; type: "admin" tokens only
kernel.admin.create(data) // { email, password, role?, courseSlugs: JSON.stringify([...]) }
kernel.admin.update(email, data)
kernel.admin.delete(email)
kernel.admin.isAdmin(email, courseSlug) // → boolean, course-scoped
kernel.admin.isSuperAdmin(email) // → boolean, role === "SUPER_ADMIN"
kernel.admin.assignCourse(email, courseSlug) // adds slug; idempotent; throws if admin or course not found
kernel.admin.unassignCourse(email, courseSlug) // removes slug; no-op if not assigned; throws if admin not foundroledefaults toADMIN.SUPER_ADMINis set explicitly at create/update.isSuperAdminandisAdminare fully separate — a super admin is not automatically authorized on a course they haven't been assigned viaassignCourse. Deliberate design choice; revisit only if a use case forces it.- Auth (
authenticate) and authorization (isAdmin/isSuperAdmin) are always two separate calls.
const admin = await kernel.admin.authenticate(token);
const allowed = await kernel.admin.isAdmin(admin.email, courseSlug);
const isSuper = await kernel.admin.isSuperAdmin(admin.email);JWT (low-level — avoid direct use)
kernel.jwt.sign(payload)
kernel.jwt.verify(token)Go through kernel.user.* / kernel.admin.* instead. No shared identity routing between User and Admin.
Course
Plain CRUD. No artifact/seed step.
kernel.course.list(filters) // { access, isActive }
kernel.course.get(slug)
kernel.course.create(data) // { slug, title, access }
kernel.course.update(slug, data)
kernel.course.delete(slug) // throws if Groups/Subscriptions still reference it
kernel.course.authorize(userId, courseSlug) // → boolean; throws/denies per tier| access tier | authorize(null, slug) | logged in, no subscription | logged in, active subscription |
|---|---|---|---|
| OPEN | ✅ | ✅ | ✅ |
| MEMBERS | ❌ throws | ✅ | ✅ |
| SUBSCRIPTION | ❌ throws | ❌ throws | ✅ |
SUBSCRIPTION tier delegates to kernel.subscription.authorize. Courses with subscription history can't be hard-deleted — retire via update(slug, { isActive: false }).
Group
Composite key (courseSlug, slug).
kernel.group.list(filters)
kernel.group.listByCourse(courseSlug)
kernel.group.get(courseSlug, slug)
kernel.group.create(data) // throws if courseSlug unresolved
kernel.group.update(courseSlug, slug, data)
kernel.group.delete(courseSlug, slug) // throws if Library rows still reference itLibrary
Authored content. Relates to Course only via Group (library.group.course).
kernel.library.list(filters, options)
kernel.library.listByCourse(courseSlug, options)
kernel.library.listByGroup(courseSlug, groupSlug, options)
kernel.library.get(slug, options)
kernel.library.create(data) // throws if (courseSlug, groupSlug) unresolved
kernel.library.update(slug, data)
kernel.library.delete(slug) // throws if Communication rows still reference it
kernel.library.createFromSlot(slug, courseSlug, groupSlug, type)Lifecycle: DRAFT → PUBLISHED → ARCHIVED. New items default DRAFT.
get()/list() return PUBLISHED only by default. Admin/preview contexts must opt in:
kernel.library.get(slug, { includeUnpublished: true });
kernel.library.list({ courseSlug }, { includeUnpublished: true });Retire content with live discussion via update(slug, { status: "ARCHIVED" }), not delete().
Communication
Discussion threads on a Library item.
kernel.communication.list(filters) // { courseSlug, librarySlug, userId, initiator, unanswered }
kernel.communication.get(id) // includes { user, library }
kernel.communication.create(data) // throws if librarySlug unresolved
kernel.communication.update(id, data)
kernel.communication.delete(id)
kernel.communication.listUnanswered(courseSlug)
kernel.communication.countUserOpenQuestions(userId)type (free-form, e.g. "user-comment") and initiator (STUDENT default | TEACHER) are independent axes.
Subscription
User's access to a Course.
kernel.subscription.list(filters)
kernel.subscription.get(id)
kernel.subscription.create(data) // { userId, courseSlug, startsAt, endsAt }
kernel.subscription.update(id, data)
kernel.subscription.delete(id)
kernel.subscription.authorize(userId, courseSlug) // throws if no ACTIVE subscriptionActive = startsAt <= now <= endsAt. Expired/not-yet-started does not authorize. Course can't be hard-deleted while any subscription history exists (including expired).
Assets — SVG, Image, Audio
Independent, no DB relation to Course/Group/Library. Referenced by slug from content.
kernel.svg.list() / .get(slug) / .create(data) / .update(slug, data) / .delete(slug)
kernel.image.list() / .get(slug) / .create(data) / .update(slug, data) / .delete(slug)
kernel.audio.list() / .get(slug) / .create(data) / .update(slug, data) / .delete(slug)| Field | Audio | Image | Svg |
|---|---|---|---|
| slug | required | required | required |
| title | optional | optional | optional |
| tags | optional | optional | optional |
| body | — | — | required |
Audio/Image have no file-path field — create() rejects unknown keys like filePath. File-on-disk mapping is convention-based, not schema-enforced. SVG is DB-only (body holds content directly).
Schema Management
npx taleem-kernel schema-check # diff app schema vs kernel canonical schema
npx taleem-kernel schema-update # copy kernel canonical schema into appRun Prisma migrate/generate after schema-update. Apps don't maintain their own Taleem Prisma models.
Design Principles
- Course → Group → Library are real relations,
onDelete: Restrictthroughout. - Course is plain CRUD — no artifact/seed step.
- Library reaches Course only via Group.
- Library lifecycle DRAFT/PUBLISHED/ARCHIVED; public reads default PUBLISHED only.
- Communication relates to Library by real relation;
type/initiatorindependent. - Subscription relates to Course by real relation; can't hard-delete Course with subscription history.
Admin.courseSlugsis a plain string, not a relation — deliberate.isAdminandisSuperAdminare separate checks — super admin status doesn't imply per-course authorization.- SVG/Image/Audio are independent assets.
- User and Admin auth are separate concerns, each owning its own token lifecycle.
- JWT is low-level; go through User/Admin modules.
