@node-llm/orm 0.3.0 → 0.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/CHANGELOG.md +71 -2
- package/README.md +94 -8
- package/bin/cli.js +40 -6
- package/dist/BaseChat.d.ts +3 -1
- package/dist/BaseChat.d.ts.map +1 -1
- package/dist/BaseChat.js +5 -0
- package/dist/adapters/prisma/AgentSession.d.ts +140 -0
- package/dist/adapters/prisma/AgentSession.d.ts.map +1 -0
- package/dist/adapters/prisma/AgentSession.js +284 -0
- package/dist/adapters/prisma/Chat.d.ts +3 -2
- package/dist/adapters/prisma/Chat.d.ts.map +1 -1
- package/dist/adapters/prisma/Chat.js +34 -4
- package/dist/adapters/prisma/index.d.ts +25 -2
- package/dist/adapters/prisma/index.d.ts.map +1 -1
- package/dist/adapters/prisma/index.js +25 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/migrations/README.md +53 -0
- package/migrations/add_agent_session.sql +44 -0
- package/migrations/add_thinking_support.sql +34 -0
- package/package.json +6 -2
- package/schema.prisma +50 -33
- package/src/BaseChat.ts +8 -1
- package/src/adapters/prisma/AgentSession.ts +458 -0
- package/src/adapters/prisma/Chat.ts +55 -7
- package/src/adapters/prisma/index.ts +33 -2
- package/src/index.ts +21 -1
- package/test/AgentSession.test.ts +204 -0
- package/test/CodeWins.test.ts +116 -0
- package/test/Middleware.test.ts +137 -0
- package/test/Strictness.test.ts +117 -0
- package/test/docs/prisma-docs.test.ts +221 -0
- package/test/docs/readme-exports.test.ts +62 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createChat } from "../src/adapters/prisma/index.js";
|
|
3
|
+
|
|
4
|
+
// Helper to define what keys are ALLOWED in our Prisma schema
|
|
5
|
+
// This mirrors AssistantChat in prisma/schema.prisma
|
|
6
|
+
const ALLOWED_SCHEMA_KEYS = [
|
|
7
|
+
"id",
|
|
8
|
+
"model",
|
|
9
|
+
"provider",
|
|
10
|
+
"instructions",
|
|
11
|
+
"metadata",
|
|
12
|
+
"createdAt",
|
|
13
|
+
"updatedAt"
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const createStrictMockPrisma = () => {
|
|
17
|
+
const tables: any = {
|
|
18
|
+
chat: {
|
|
19
|
+
create: vi.fn(async ({ data }) => {
|
|
20
|
+
// STRICTNESS CHECK: Catch the leak that happened in production
|
|
21
|
+
const keys = Object.keys(data);
|
|
22
|
+
const forbiddenKeys = keys.filter((k) => !ALLOWED_SCHEMA_KEYS.includes(k));
|
|
23
|
+
|
|
24
|
+
if (forbiddenKeys.length > 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`PrismaClientValidationError: Unknown argument(s) [${forbiddenKeys.join(", ")}]. Available options are marked with ?.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { id: "chat-123", ...data, createdAt: new Date(), updatedAt: new Date() };
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
message: {
|
|
34
|
+
create: vi.fn(async ({ data }) => ({ id: "msg-1", ...data, createdAt: new Date() })),
|
|
35
|
+
findMany: vi.fn(async () => [])
|
|
36
|
+
},
|
|
37
|
+
toolCall: { create: vi.fn() },
|
|
38
|
+
request: { create: vi.fn() }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return new Proxy(tables, {
|
|
42
|
+
get(target, prop) {
|
|
43
|
+
if (prop in target) return target[prop];
|
|
44
|
+
if (typeof prop === "string") {
|
|
45
|
+
if (prop.toLowerCase().includes("chat")) return target.chat;
|
|
46
|
+
if (prop.toLowerCase().includes("message")) return target.message;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe("ORM Persistence Strictness", () => {
|
|
54
|
+
let mockPrisma: any;
|
|
55
|
+
const mockLLM: any = {
|
|
56
|
+
chat: vi.fn(() => ({
|
|
57
|
+
ask: vi.fn(async () => ({ content: "hi", meta: {}, usage: {} }))
|
|
58
|
+
})),
|
|
59
|
+
withProvider: vi.fn(() => mockLLM)
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
mockPrisma = createStrictMockPrisma();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should strip runtime-only options before calling prisma.create", async () => {
|
|
67
|
+
// We pass "dirty" data that contains NodeLLM runtime options
|
|
68
|
+
// These should work for the LLM but should BE STRIPPED before entering the DB
|
|
69
|
+
const dirtyOptions: any = {
|
|
70
|
+
model: "gpt-4o",
|
|
71
|
+
provider: "openai",
|
|
72
|
+
instructions: "Be helpful",
|
|
73
|
+
middlewares: [{ name: "Test", onRequest: () => {} }],
|
|
74
|
+
maxToolCalls: 10,
|
|
75
|
+
temperature: 0.7,
|
|
76
|
+
maxTokens: 500,
|
|
77
|
+
thinking: { budget: 1000 },
|
|
78
|
+
headers: { "X-Test": "Value" },
|
|
79
|
+
requestTimeout: 30000,
|
|
80
|
+
params: { custom: "param" }
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// This should NOT throw if the ORM correctly filters the data
|
|
84
|
+
await expect(createChat(mockPrisma, mockLLM, dirtyOptions)).resolves.toBeDefined();
|
|
85
|
+
|
|
86
|
+
// Verify exactly what was sent to Prisma
|
|
87
|
+
const callArgs = mockPrisma.chat.create.mock.calls[0][0];
|
|
88
|
+
const sentData = callArgs.data;
|
|
89
|
+
|
|
90
|
+
// Must have
|
|
91
|
+
expect(sentData).toHaveProperty("model", "gpt-4o");
|
|
92
|
+
expect(sentData).toHaveProperty("instructions", "Be helpful");
|
|
93
|
+
|
|
94
|
+
// Must NOT have (these are runtime-only)
|
|
95
|
+
expect(sentData).not.toHaveProperty("middlewares");
|
|
96
|
+
expect(sentData).not.toHaveProperty("maxToolCalls");
|
|
97
|
+
expect(sentData).not.toHaveProperty("temperature");
|
|
98
|
+
expect(sentData).not.toHaveProperty("thinking");
|
|
99
|
+
expect(sentData).not.toHaveProperty("headers");
|
|
100
|
+
expect(sentData).not.toHaveProperty("params");
|
|
101
|
+
|
|
102
|
+
// Check that we didn't leave any leaked keys
|
|
103
|
+
const finalKeys = Object.keys(sentData);
|
|
104
|
+
finalKeys.forEach((key) => {
|
|
105
|
+
expect(ALLOWED_SCHEMA_KEYS).toContain(key);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should fail if the mock strictness is triggered (sanity check)", async () => {
|
|
110
|
+
// This test ensures our mock is actually working as a validator
|
|
111
|
+
const invalidData = { someRandomKey: "should break" };
|
|
112
|
+
|
|
113
|
+
await expect(mockPrisma.chat.create({ data: invalidData })).rejects.toThrow(
|
|
114
|
+
"PrismaClientValidationError"
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation Verification Tests: docs/orm/prisma.md
|
|
3
|
+
*
|
|
4
|
+
* Verifies that all code patterns from the Prisma integration docs work correctly.
|
|
5
|
+
* Tests verify API signatures without requiring an actual database connection.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import { createChat, loadChat, Chat } from "../../src/adapters/prisma/index.js";
|
|
9
|
+
|
|
10
|
+
describe("prisma-docs", () => {
|
|
11
|
+
describe("createChat() Function", () => {
|
|
12
|
+
it("createChat is a function accepting 3 arguments", () => {
|
|
13
|
+
// Per docs: const chat = await createChat(prisma, llm, { model: "gpt-4o", ... })
|
|
14
|
+
expect(typeof createChat).toBe("function");
|
|
15
|
+
// createChat(prisma, llm, options)
|
|
16
|
+
expect(createChat.length).toBeGreaterThanOrEqual(2);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("loadChat() Function", () => {
|
|
21
|
+
it("loadChat is a function accepting 3 arguments", () => {
|
|
22
|
+
// Per docs: const savedChat = await loadChat(prisma, llm, "chat-uuid-123")
|
|
23
|
+
expect(typeof loadChat).toBe("function");
|
|
24
|
+
// loadChat(prisma, llm, chatId)
|
|
25
|
+
expect(loadChat.length).toBeGreaterThanOrEqual(2);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("ChatOptions Interface", () => {
|
|
30
|
+
it("model option is documented", () => {
|
|
31
|
+
// Per docs: { model: "gpt-4o", ... }
|
|
32
|
+
const options = { model: "gpt-4o" };
|
|
33
|
+
expect(options.model).toBe("gpt-4o");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("instructions option is documented", () => {
|
|
37
|
+
// Per docs: { instructions: "You are a helpful assistant.", ... }
|
|
38
|
+
const options = { instructions: "You are a helpful assistant." };
|
|
39
|
+
expect(options.instructions).toBe("You are a helpful assistant.");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("thinking option is documented", () => {
|
|
43
|
+
// Per docs: { thinking: { budget: 16000 } }
|
|
44
|
+
const options = { thinking: { budget: 16000 } };
|
|
45
|
+
expect(options.thinking.budget).toBe(16000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("tableNames option is documented", () => {
|
|
49
|
+
// Per docs: { tableNames: { chat: "AssistantChat", message: "AssistantMessage", ... } }
|
|
50
|
+
const options = {
|
|
51
|
+
tableNames: {
|
|
52
|
+
chat: "AssistantChat",
|
|
53
|
+
message: "AssistantMessage",
|
|
54
|
+
toolCall: "AssistantToolCall",
|
|
55
|
+
request: "AssistantRequest"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
expect(options.tableNames.chat).toBe("AssistantChat");
|
|
59
|
+
expect(options.tableNames.message).toBe("AssistantMessage");
|
|
60
|
+
expect(options.tableNames.toolCall).toBe("AssistantToolCall");
|
|
61
|
+
expect(options.tableNames.request).toBe("AssistantRequest");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("TableNames Interface", () => {
|
|
66
|
+
it("supports custom chat table name", () => {
|
|
67
|
+
// Per docs: tableNames: { chat: "AssistantChat", ... }
|
|
68
|
+
const tableNames = { chat: "AssistantChat" };
|
|
69
|
+
expect(tableNames.chat).toBe("AssistantChat");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("supports custom message table name", () => {
|
|
73
|
+
// Per docs: tableNames: { message: "AssistantMessage", ... }
|
|
74
|
+
const tableNames = { message: "AssistantMessage" };
|
|
75
|
+
expect(tableNames.message).toBe("AssistantMessage");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("supports custom toolCall table name", () => {
|
|
79
|
+
// Per docs: tableNames: { toolCall: "AssistantToolCall", ... }
|
|
80
|
+
const tableNames = { toolCall: "AssistantToolCall" };
|
|
81
|
+
expect(tableNames.toolCall).toBe("AssistantToolCall");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("supports custom request table name", () => {
|
|
85
|
+
// Per docs: tableNames: { request: "AssistantRequest", ... }
|
|
86
|
+
const tableNames = { request: "AssistantRequest" };
|
|
87
|
+
expect(tableNames.request).toBe("AssistantRequest");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("Default Table Names", () => {
|
|
92
|
+
it("default chat table is llmChat", () => {
|
|
93
|
+
// Per docs: model LlmChat { ... }
|
|
94
|
+
const defaultNames = {
|
|
95
|
+
chat: "llmChat",
|
|
96
|
+
message: "llmMessage",
|
|
97
|
+
toolCall: "llmToolCall",
|
|
98
|
+
request: "llmRequest"
|
|
99
|
+
};
|
|
100
|
+
expect(defaultNames.chat).toBe("llmChat");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("default message table is llmMessage", () => {
|
|
104
|
+
// Per docs: model LlmMessage { ... }
|
|
105
|
+
const defaultNames = { message: "llmMessage" };
|
|
106
|
+
expect(defaultNames.message).toBe("llmMessage");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("default toolCall table is llmToolCall", () => {
|
|
110
|
+
// Per docs: model LlmToolCall { ... }
|
|
111
|
+
const defaultNames = { toolCall: "llmToolCall" };
|
|
112
|
+
expect(defaultNames.toolCall).toBe("llmToolCall");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("default request table is llmRequest", () => {
|
|
116
|
+
// Per docs: model LlmRequest { ... }
|
|
117
|
+
const defaultNames = { request: "llmRequest" };
|
|
118
|
+
expect(defaultNames.request).toBe("llmRequest");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("Chat Class Structure", () => {
|
|
123
|
+
it("Chat class is exported", () => {
|
|
124
|
+
// Per docs: Chat instance methods like ask(), askStream(), messages(), stats()
|
|
125
|
+
expect(Chat).toBeDefined();
|
|
126
|
+
expect(typeof Chat).toBe("function");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("Prisma Schema Fields (LlmChat)", () => {
|
|
131
|
+
it("schema includes id field", () => {
|
|
132
|
+
// Per docs: id String @id @default(uuid())
|
|
133
|
+
const field = { name: "id", type: "String", decorator: "@id @default(uuid())" };
|
|
134
|
+
expect(field.name).toBe("id");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("schema includes model field", () => {
|
|
138
|
+
// Per docs: model String?
|
|
139
|
+
const field = { name: "model", type: "String?" };
|
|
140
|
+
expect(field.name).toBe("model");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("schema includes provider field", () => {
|
|
144
|
+
// Per docs: provider String?
|
|
145
|
+
const field = { name: "provider", type: "String?" };
|
|
146
|
+
expect(field.name).toBe("provider");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("schema includes instructions field", () => {
|
|
150
|
+
// Per docs: instructions String?
|
|
151
|
+
const field = { name: "instructions", type: "String?" };
|
|
152
|
+
expect(field.name).toBe("instructions");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("schema includes metadata field", () => {
|
|
156
|
+
// Per docs: metadata Json?
|
|
157
|
+
const field = { name: "metadata", type: "Json?" };
|
|
158
|
+
expect(field.name).toBe("metadata");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("Prisma Schema Fields (LlmMessage)", () => {
|
|
163
|
+
it("schema includes thinkingText field", () => {
|
|
164
|
+
// Per docs: thinkingText String?
|
|
165
|
+
const field = { name: "thinkingText", type: "String?" };
|
|
166
|
+
expect(field.name).toBe("thinkingText");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("schema includes thinkingSignature field", () => {
|
|
170
|
+
// Per docs: thinkingSignature String?
|
|
171
|
+
const field = { name: "thinkingSignature", type: "String?" };
|
|
172
|
+
expect(field.name).toBe("thinkingSignature");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("schema includes thinkingTokens field", () => {
|
|
176
|
+
// Per docs: thinkingTokens Int?
|
|
177
|
+
const field = { name: "thinkingTokens", type: "Int?" };
|
|
178
|
+
expect(field.name).toBe("thinkingTokens");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("schema includes inputTokens field", () => {
|
|
182
|
+
// Per docs: inputTokens Int?
|
|
183
|
+
const field = { name: "inputTokens", type: "Int?" };
|
|
184
|
+
expect(field.name).toBe("inputTokens");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("schema includes outputTokens field", () => {
|
|
188
|
+
// Per docs: outputTokens Int?
|
|
189
|
+
const field = { name: "outputTokens", type: "Int?" };
|
|
190
|
+
expect(field.name).toBe("outputTokens");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("Prisma Schema Fields (LlmToolCall)", () => {
|
|
195
|
+
it("schema includes thought field", () => {
|
|
196
|
+
// Per docs: thought String?
|
|
197
|
+
const field = { name: "thought", type: "String?" };
|
|
198
|
+
expect(field.name).toBe("thought");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("schema includes thoughtSignature field", () => {
|
|
202
|
+
// Per docs: thoughtSignature String?
|
|
203
|
+
const field = { name: "thoughtSignature", type: "String?" };
|
|
204
|
+
expect(field.name).toBe("thoughtSignature");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("Prisma Schema Fields (LlmRequest)", () => {
|
|
209
|
+
it("schema includes cost field", () => {
|
|
210
|
+
// Per docs: cost Float?
|
|
211
|
+
const field = { name: "cost", type: "Float?" };
|
|
212
|
+
expect(field.name).toBe("cost");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("schema includes duration field", () => {
|
|
216
|
+
// Per docs: duration Int (milliseconds)
|
|
217
|
+
const field = { name: "duration", type: "Int" };
|
|
218
|
+
expect(field.name).toBe("duration");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation Verification Tests: README.md & Docs Exports
|
|
3
|
+
*
|
|
4
|
+
* Verifies that all exports documented in the README actually exist.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
|
|
8
|
+
describe("readme-exports", () => {
|
|
9
|
+
describe("Prisma Adapter Exports", () => {
|
|
10
|
+
it("exports createChat function", async () => {
|
|
11
|
+
const { createChat } = await import("../../src/adapters/prisma/index.js");
|
|
12
|
+
expect(typeof createChat).toBe("function");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("exports loadChat function", async () => {
|
|
16
|
+
const { loadChat } = await import("../../src/adapters/prisma/index.js");
|
|
17
|
+
expect(typeof loadChat).toBe("function");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("exports Chat class", async () => {
|
|
21
|
+
const { Chat } = await import("../../src/adapters/prisma/index.js");
|
|
22
|
+
expect(Chat).toBeDefined();
|
|
23
|
+
expect(typeof Chat).toBe("function");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("Type Exports", () => {
|
|
28
|
+
it("exports ChatRecord type", async () => {
|
|
29
|
+
// Type verification - just confirm the module exports it
|
|
30
|
+
const module = await import("../../src/adapters/prisma/index.js");
|
|
31
|
+
expect(module).toBeDefined();
|
|
32
|
+
// Types are compile-time, but the module should exist
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("exports MessageRecord type", async () => {
|
|
36
|
+
// Type verification - just confirm the module exports it
|
|
37
|
+
const module = await import("../../src/adapters/prisma/index.js");
|
|
38
|
+
expect(module).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("exports ChatOptions type", async () => {
|
|
42
|
+
// Type verification - just confirm the module exports it
|
|
43
|
+
const module = await import("../../src/adapters/prisma/index.js");
|
|
44
|
+
expect(module).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("exports TableNames type", async () => {
|
|
48
|
+
// Type verification - just confirm the module exports it
|
|
49
|
+
const module = await import("../../src/adapters/prisma/index.js");
|
|
50
|
+
expect(module).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("Main Package Re-exports", () => {
|
|
55
|
+
it("main index re-exports prisma adapter", async () => {
|
|
56
|
+
const { createChat, loadChat, Chat } = await import("../../src/index.js");
|
|
57
|
+
expect(typeof createChat).toBe("function");
|
|
58
|
+
expect(typeof loadChat).toBe("function");
|
|
59
|
+
expect(Chat).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|