@studious-lms/server 1.26.0 → 1.27.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/.env.example +3 -1
- package/.github/workflows/npm-publish.yml +1 -0
- package/AGENTS.md +84 -0
- package/dist/models/cloudStorage.d.ts +11 -0
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/cloudStorage.js +18 -2
- package/dist/models/cloudStorage.js.map +1 -1
- package/dist/models/conversation.d.ts +1 -0
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +40 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/labChat.js +32 -2
- package/dist/models/labChat.js.map +1 -1
- package/dist/models/message.d.ts +3 -0
- package/dist/models/message.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +2 -0
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +60 -16
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +170 -2
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +30 -2
- package/dist/pipelines/aiLabChatContract.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +30 -5
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +1 -1
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +13 -14
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/routers/_app.d.ts +220 -10
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/admin.d.ts +0 -1
- package/dist/routers/admin.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +1 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/labChat.d.ts +10 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +6 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/message.d.ts +37 -4
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +24 -7
- package/dist/routers/message.js.map +1 -1
- package/dist/routers/parent.d.ts +18 -0
- package/dist/routers/parent.d.ts.map +1 -1
- package/dist/routers/parent.js +10 -3
- package/dist/routers/parent.js.map +1 -1
- package/dist/routers/studentProgress.d.ts +44 -0
- package/dist/routers/studentProgress.d.ts.map +1 -1
- package/dist/routers/studentProgress.js +12 -3
- package/dist/routers/studentProgress.js.map +1 -1
- package/dist/routes/aiStream.js +32 -2
- package/dist/routes/aiStream.js.map +1 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +107 -3
- package/dist/seedDatabase.js.map +1 -1
- package/dist/services/adminUser.d.ts +0 -1
- package/dist/services/adminUser.d.ts.map +1 -1
- package/dist/services/adminUser.js +7 -4
- package/dist/services/adminUser.js.map +1 -1
- package/dist/services/aiChatStream.d.ts +15 -0
- package/dist/services/aiChatStream.d.ts.map +1 -1
- package/dist/services/aiChatStream.js +144 -74
- package/dist/services/aiChatStream.js.map +1 -1
- package/dist/services/aiGeneration.d.ts +4 -0
- package/dist/services/aiGeneration.d.ts.map +1 -0
- package/dist/services/aiGeneration.js +88 -0
- package/dist/services/aiGeneration.js.map +1 -0
- package/dist/services/aiProposalTools.d.ts +4 -1
- package/dist/services/aiProposalTools.d.ts.map +1 -1
- package/dist/services/aiProposalTools.js +128 -3
- package/dist/services/aiProposalTools.js.map +1 -1
- package/dist/services/aiProposals.d.ts +70 -0
- package/dist/services/aiProposals.d.ts.map +1 -0
- package/dist/services/aiProposals.js +363 -0
- package/dist/services/aiProposals.js.map +1 -0
- package/dist/services/conversation.d.ts +1 -0
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/labChat.d.ts +10 -0
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +52 -3
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +24 -8
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +170 -117
- package/dist/services/message.js.map +1 -1
- package/dist/services/parentPortal.d.ts +14 -0
- package/dist/services/parentPortal.d.ts.map +1 -1
- package/dist/services/parentPortal.js +72 -2
- package/dist/services/parentPortal.js.map +1 -1
- package/dist/services/studentProgress.d.ts +50 -0
- package/dist/services/studentProgress.d.ts.map +1 -1
- package/dist/services/studentProgress.js +107 -2
- package/dist/services/studentProgress.js.map +1 -1
- package/dist/utils/inference.d.ts +10 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +77 -2
- package/dist/utils/inference.js.map +1 -1
- package/dist/workers/processors/createRunJobWorker.js +17 -16
- package/dist/workers/processors/createRunJobWorker.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260811130000_add_ai_message_feedback/migration.sql +7 -0
- package/prisma/schema.prisma +8 -0
- package/src/models/cloudStorage.ts +19 -0
- package/src/models/labChat.ts +34 -0
- package/src/pipelines/aiLabChat.ts +79 -18
- package/src/pipelines/aiLabChatContract.ts +35 -0
- package/src/pipelines/aiNewtonChat.ts +34 -2
- package/src/pipelines/labChatPrompt.ts +11 -12
- package/src/routers/labChat.ts +5 -0
- package/src/routers/message.ts +34 -5
- package/src/routers/parent.ts +11 -0
- package/src/routers/studentProgress.ts +15 -0
- package/src/routes/aiStream.ts +32 -0
- package/src/seedDatabase.ts +107 -2
- package/src/services/adminUser.ts +5 -2
- package/src/services/aiChatStream.ts +83 -7
- package/src/services/aiGeneration.ts +92 -0
- package/src/services/aiProposalTools.ts +166 -1
- package/src/services/aiProposals.ts +428 -0
- package/src/services/labChat.ts +65 -0
- package/src/services/message.ts +205 -142
- package/src/services/parentPortal.ts +85 -0
- package/src/services/studentProgress.ts +135 -0
- package/src/utils/inference.ts +85 -0
- package/src/workers/processors/createRunJobWorker.ts +17 -17
- package/tests/unit/services/adminUser.test.ts +75 -0
- package/tests/unit/utils/adminPanelAccess.test.ts +26 -0
|
@@ -91,6 +91,14 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
91
91
|
return existing.result;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
if (existing.status === 'CANCELLED') {
|
|
95
|
+
logger.info(`${name} RunJob cancelled before start; skipping`, {
|
|
96
|
+
runJobId,
|
|
97
|
+
bullJobId: job.id,
|
|
98
|
+
});
|
|
99
|
+
return existing.result;
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
await prisma.runJob.update({
|
|
95
103
|
where: { id: runJobId },
|
|
96
104
|
data: {
|
|
@@ -108,8 +116,10 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
108
116
|
classId: existing.classId ?? null,
|
|
109
117
|
});
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
// `updateMany` with a status guard so a cancellation that landed while
|
|
120
|
+
// the run was in flight is not overwritten with SUCCEEDED.
|
|
121
|
+
await prisma.runJob.updateMany({
|
|
122
|
+
where: { id: runJobId, status: { not: 'CANCELLED' } },
|
|
113
123
|
data: {
|
|
114
124
|
status: 'SUCCEEDED',
|
|
115
125
|
finishedAt: new Date(),
|
|
@@ -146,29 +156,19 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
146
156
|
|
|
147
157
|
if (!runJobId) return;
|
|
148
158
|
|
|
159
|
+
// Status guard: don't let a late failure (e.g. from an aborted stream)
|
|
160
|
+
// overwrite a RunJob the user already cancelled.
|
|
149
161
|
await prisma.runJob
|
|
150
|
-
.
|
|
151
|
-
where: { id: runJobId },
|
|
162
|
+
.updateMany({
|
|
163
|
+
where: { id: runJobId, status: { not: 'CANCELLED' } },
|
|
152
164
|
data: {
|
|
153
165
|
error: err.message,
|
|
154
166
|
...(isFinalAttempt
|
|
155
|
-
? { status: 'FAILED', finishedAt: new Date() }
|
|
167
|
+
? { status: 'FAILED' as const, finishedAt: new Date() }
|
|
156
168
|
: {}),
|
|
157
169
|
},
|
|
158
170
|
})
|
|
159
171
|
.catch((updateError) => {
|
|
160
|
-
// P2025 = record not found. The RunJob row is gone (orphaned job from a
|
|
161
|
-
// shared/reset Redis), so there's nothing to flag as FAILED — log
|
|
162
|
-
// quietly instead of as an error to avoid noise.
|
|
163
|
-
if (
|
|
164
|
-
updateError instanceof Prisma.PrismaClientKnownRequestError &&
|
|
165
|
-
updateError.code === 'P2025'
|
|
166
|
-
) {
|
|
167
|
-
logger.warn(`${name} failure not recorded; RunJob no longer exists`, {
|
|
168
|
-
runJobId,
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
172
|
logger.error(`Failed to record ${name} failure on RunJob`, {
|
|
173
173
|
runJobId,
|
|
174
174
|
error:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { TRPCError } from '@trpc/server';
|
|
3
|
+
|
|
4
|
+
const prismaMocks = vi.hoisted(() => ({
|
|
5
|
+
user: { findUnique: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../../src/lib/prisma.js', () => ({
|
|
9
|
+
prisma: prismaMocks,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { getUserForAdmin } from '../../../src/services/adminUser.js';
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('getUserForAdmin', () => {
|
|
19
|
+
test('does not select session ids (they are bearer tokens) and only returns AUTH sessions', async () => {
|
|
20
|
+
prismaMocks.user.findUnique.mockResolvedValue({
|
|
21
|
+
id: 'user-1',
|
|
22
|
+
username: 'ada',
|
|
23
|
+
email: 'ada@example.com',
|
|
24
|
+
verified: true,
|
|
25
|
+
termsAcceptedAt: null,
|
|
26
|
+
termsVersion: null,
|
|
27
|
+
profile: null,
|
|
28
|
+
school: null,
|
|
29
|
+
schoolMemberships: [],
|
|
30
|
+
sessions: [
|
|
31
|
+
{
|
|
32
|
+
type: 'AUTH',
|
|
33
|
+
createdAt: new Date('2026-08-01T00:00:00Z'),
|
|
34
|
+
expiresAt: new Date('2026-09-01T00:00:00Z'),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
_count: {
|
|
38
|
+
teacherIn: 0,
|
|
39
|
+
studentIn: 0,
|
|
40
|
+
sessions: 1,
|
|
41
|
+
submissions: 0,
|
|
42
|
+
files: 0,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const user = await getUserForAdmin('user-1');
|
|
47
|
+
|
|
48
|
+
expect(prismaMocks.user.findUnique).toHaveBeenCalledWith(
|
|
49
|
+
expect.objectContaining({
|
|
50
|
+
where: { id: 'user-1' },
|
|
51
|
+
select: expect.objectContaining({
|
|
52
|
+
sessions: {
|
|
53
|
+
where: { type: 'AUTH' },
|
|
54
|
+
orderBy: { createdAt: 'desc' },
|
|
55
|
+
take: 10,
|
|
56
|
+
select: { type: true, createdAt: true, expiresAt: true },
|
|
57
|
+
},
|
|
58
|
+
_count: {
|
|
59
|
+
select: expect.objectContaining({
|
|
60
|
+
sessions: { where: { type: 'AUTH' } },
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
expect(user.sessions[0]).not.toHaveProperty('id');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('throws NOT_FOUND when the user is missing', async () => {
|
|
70
|
+
prismaMocks.user.findUnique.mockResolvedValue(null);
|
|
71
|
+
await expect(getUserForAdmin('missing')).rejects.toMatchObject({
|
|
72
|
+
code: 'NOT_FOUND',
|
|
73
|
+
} satisfies Partial<TRPCError>);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { TRPCError } from '@trpc/server';
|
|
3
|
+
import type { Request } from 'express';
|
|
4
|
+
import { assertAdminPanelPassword } from '../../../src/utils/adminPanelAccess.js';
|
|
5
|
+
|
|
6
|
+
function reqWithHeader(value: string | string[] | undefined): Request {
|
|
7
|
+
return { headers: { 'x-admin-password': value } } as unknown as Request;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('assertAdminPanelPassword', () => {
|
|
11
|
+
test('accepts the configured admin password header', () => {
|
|
12
|
+
expect(() => assertAdminPanelPassword(reqWithHeader('alan6767'))).not.toThrow();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('rejects a missing or wrong header with FORBIDDEN', () => {
|
|
16
|
+
expect(() => assertAdminPanelPassword(reqWithHeader(undefined))).toThrow(
|
|
17
|
+
TRPCError,
|
|
18
|
+
);
|
|
19
|
+
try {
|
|
20
|
+
assertAdminPanelPassword(reqWithHeader('nope'));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
expect(error).toBeInstanceOf(TRPCError);
|
|
23
|
+
expect((error as TRPCError).code).toBe('FORBIDDEN');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|