@logtape/drizzle-orm 1.4.0-dev.409 → 1.4.0-dev.413
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/package.json +5 -2
- package/deno.json +0 -34
- package/src/mod.test.ts +0 -632
- package/src/mod.ts +0 -188
- package/tsdown.config.ts +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/drizzle-orm",
|
|
3
|
-
"version": "1.4.0-dev.
|
|
3
|
+
"version": "1.4.0-dev.413+1097da33",
|
|
4
4
|
"description": "Drizzle ORM adapter for LogTape logging library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logging",
|
|
@@ -48,8 +48,11 @@
|
|
|
48
48
|
"./package.json": "./package.json"
|
|
49
49
|
},
|
|
50
50
|
"sideEffects": false,
|
|
51
|
+
"files": [
|
|
52
|
+
"dist/"
|
|
53
|
+
],
|
|
51
54
|
"peerDependencies": {
|
|
52
|
-
"@logtape/logtape": "^1.4.0-dev.
|
|
55
|
+
"@logtape/logtape": "^1.4.0-dev.413+1097da33"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
55
58
|
"@alinea/suite": "^0.6.3",
|
package/deno.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@logtape/drizzle-orm",
|
|
3
|
-
"version": "1.4.0-dev.409+63c2cd45",
|
|
4
|
-
"license": "MIT",
|
|
5
|
-
"exports": "./src/mod.ts",
|
|
6
|
-
"exclude": [
|
|
7
|
-
"coverage/",
|
|
8
|
-
"npm/",
|
|
9
|
-
"dist/"
|
|
10
|
-
],
|
|
11
|
-
"tasks": {
|
|
12
|
-
"build": "pnpm build",
|
|
13
|
-
"test": "deno test --allow-env --allow-sys --allow-net",
|
|
14
|
-
"test:node": {
|
|
15
|
-
"dependencies": [
|
|
16
|
-
"build"
|
|
17
|
-
],
|
|
18
|
-
"command": "node --experimental-transform-types --test"
|
|
19
|
-
},
|
|
20
|
-
"test:bun": {
|
|
21
|
-
"dependencies": [
|
|
22
|
-
"build"
|
|
23
|
-
],
|
|
24
|
-
"command": "bun test"
|
|
25
|
-
},
|
|
26
|
-
"test-all": {
|
|
27
|
-
"dependencies": [
|
|
28
|
-
"test",
|
|
29
|
-
"test:node",
|
|
30
|
-
"test:bun"
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/mod.test.ts
DELETED
|
@@ -1,632 +0,0 @@
|
|
|
1
|
-
import { suite } from "@alinea/suite";
|
|
2
|
-
import { assertEquals } from "@std/assert/equals";
|
|
3
|
-
import { configure, type LogRecord, reset } from "@logtape/logtape";
|
|
4
|
-
import { DrizzleLogger, getLogger, serialize, stringLiteral } from "./mod.ts";
|
|
5
|
-
|
|
6
|
-
const test = suite(import.meta);
|
|
7
|
-
|
|
8
|
-
// Test fixture: Collect log records, filtering out internal LogTape meta logs
|
|
9
|
-
function createTestSink(): {
|
|
10
|
-
sink: (record: LogRecord) => void;
|
|
11
|
-
logs: LogRecord[];
|
|
12
|
-
} {
|
|
13
|
-
const logs: LogRecord[] = [];
|
|
14
|
-
return {
|
|
15
|
-
// Filter out logtape meta logs automatically
|
|
16
|
-
sink: (record: LogRecord) => {
|
|
17
|
-
if (record.category[0] !== "logtape") {
|
|
18
|
-
logs.push(record);
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
logs,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Setup helper
|
|
26
|
-
async function setupLogtape(): Promise<{
|
|
27
|
-
logs: LogRecord[];
|
|
28
|
-
cleanup: () => Promise<void>;
|
|
29
|
-
}> {
|
|
30
|
-
const { sink, logs } = createTestSink();
|
|
31
|
-
await configure({
|
|
32
|
-
sinks: { test: sink },
|
|
33
|
-
loggers: [{ category: [], sinks: ["test"] }],
|
|
34
|
-
});
|
|
35
|
-
return { logs, cleanup: () => reset() };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================
|
|
39
|
-
// Basic Logger Creation Tests
|
|
40
|
-
// ============================================
|
|
41
|
-
|
|
42
|
-
test("getLogger(): creates a Drizzle ORM-compatible logger", async () => {
|
|
43
|
-
const { cleanup } = await setupLogtape();
|
|
44
|
-
try {
|
|
45
|
-
const logger = getLogger();
|
|
46
|
-
|
|
47
|
-
assertEquals(typeof logger.logQuery, "function");
|
|
48
|
-
assertEquals(logger instanceof DrizzleLogger, true);
|
|
49
|
-
} finally {
|
|
50
|
-
await cleanup();
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("getLogger(): uses default category ['drizzle-orm']", async () => {
|
|
55
|
-
const { logs, cleanup } = await setupLogtape();
|
|
56
|
-
try {
|
|
57
|
-
const logger = getLogger();
|
|
58
|
-
logger.logQuery("SELECT 1", []);
|
|
59
|
-
|
|
60
|
-
assertEquals(logs.length, 1);
|
|
61
|
-
assertEquals(logs[0].category, ["drizzle-orm"]);
|
|
62
|
-
} finally {
|
|
63
|
-
await cleanup();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("getLogger(): uses custom category array", async () => {
|
|
68
|
-
const { logs, cleanup } = await setupLogtape();
|
|
69
|
-
try {
|
|
70
|
-
const logger = getLogger({ category: ["myapp", "database"] });
|
|
71
|
-
logger.logQuery("SELECT 1", []);
|
|
72
|
-
|
|
73
|
-
assertEquals(logs.length, 1);
|
|
74
|
-
assertEquals(logs[0].category, ["myapp", "database"]);
|
|
75
|
-
} finally {
|
|
76
|
-
await cleanup();
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("getLogger(): accepts string category", async () => {
|
|
81
|
-
const { logs, cleanup } = await setupLogtape();
|
|
82
|
-
try {
|
|
83
|
-
const logger = getLogger({ category: "database" });
|
|
84
|
-
logger.logQuery("SELECT 1", []);
|
|
85
|
-
|
|
86
|
-
assertEquals(logs.length, 1);
|
|
87
|
-
assertEquals(logs[0].category, ["database"]);
|
|
88
|
-
} finally {
|
|
89
|
-
await cleanup();
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ============================================
|
|
94
|
-
// Log Level Tests
|
|
95
|
-
// ============================================
|
|
96
|
-
|
|
97
|
-
test("getLogger(): uses default level 'debug'", async () => {
|
|
98
|
-
const { logs, cleanup } = await setupLogtape();
|
|
99
|
-
try {
|
|
100
|
-
const logger = getLogger();
|
|
101
|
-
logger.logQuery("SELECT 1", []);
|
|
102
|
-
|
|
103
|
-
assertEquals(logs.length, 1);
|
|
104
|
-
assertEquals(logs[0].level, "debug");
|
|
105
|
-
} finally {
|
|
106
|
-
await cleanup();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("getLogger(): uses custom level", async () => {
|
|
111
|
-
const { logs, cleanup } = await setupLogtape();
|
|
112
|
-
try {
|
|
113
|
-
const logger = getLogger({ level: "info" });
|
|
114
|
-
logger.logQuery("SELECT 1", []);
|
|
115
|
-
|
|
116
|
-
assertEquals(logs.length, 1);
|
|
117
|
-
assertEquals(logs[0].level, "info");
|
|
118
|
-
} finally {
|
|
119
|
-
await cleanup();
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("getLogger(): supports all log levels", async () => {
|
|
124
|
-
const { logs, cleanup } = await setupLogtape();
|
|
125
|
-
try {
|
|
126
|
-
const levels = [
|
|
127
|
-
"trace",
|
|
128
|
-
"debug",
|
|
129
|
-
"info",
|
|
130
|
-
"warning",
|
|
131
|
-
"error",
|
|
132
|
-
"fatal",
|
|
133
|
-
] as const;
|
|
134
|
-
|
|
135
|
-
for (const level of levels) {
|
|
136
|
-
logs.length = 0; // Clear logs
|
|
137
|
-
const logger = getLogger({ level });
|
|
138
|
-
logger.logQuery("SELECT 1", []);
|
|
139
|
-
|
|
140
|
-
assertEquals(logs.length, 1);
|
|
141
|
-
assertEquals(logs[0].level, level);
|
|
142
|
-
}
|
|
143
|
-
} finally {
|
|
144
|
-
await cleanup();
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// ============================================
|
|
149
|
-
// Query Logging Tests
|
|
150
|
-
// ============================================
|
|
151
|
-
|
|
152
|
-
test("logQuery(): logs query with structured data", async () => {
|
|
153
|
-
const { logs, cleanup } = await setupLogtape();
|
|
154
|
-
try {
|
|
155
|
-
const logger = getLogger();
|
|
156
|
-
logger.logQuery("SELECT * FROM users WHERE id = $1", ["123"]);
|
|
157
|
-
|
|
158
|
-
assertEquals(logs.length, 1);
|
|
159
|
-
assertEquals(logs[0].properties.query, "SELECT * FROM users WHERE id = $1");
|
|
160
|
-
assertEquals(logs[0].properties.params, ["123"]);
|
|
161
|
-
assertEquals(
|
|
162
|
-
logs[0].properties.formattedQuery,
|
|
163
|
-
"SELECT * FROM users WHERE id = '123'",
|
|
164
|
-
);
|
|
165
|
-
} finally {
|
|
166
|
-
await cleanup();
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("logQuery(): formats query with multiple parameters", async () => {
|
|
171
|
-
const { logs, cleanup } = await setupLogtape();
|
|
172
|
-
try {
|
|
173
|
-
const logger = getLogger();
|
|
174
|
-
logger.logQuery(
|
|
175
|
-
"SELECT * FROM users WHERE name = $1 AND age > $2",
|
|
176
|
-
["Alice", 25],
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
assertEquals(logs.length, 1);
|
|
180
|
-
assertEquals(
|
|
181
|
-
logs[0].properties.formattedQuery,
|
|
182
|
-
"SELECT * FROM users WHERE name = 'Alice' AND age > 25",
|
|
183
|
-
);
|
|
184
|
-
} finally {
|
|
185
|
-
await cleanup();
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("logQuery(): handles empty parameters", async () => {
|
|
190
|
-
const { logs, cleanup } = await setupLogtape();
|
|
191
|
-
try {
|
|
192
|
-
const logger = getLogger();
|
|
193
|
-
logger.logQuery("SELECT * FROM users", []);
|
|
194
|
-
|
|
195
|
-
assertEquals(logs.length, 1);
|
|
196
|
-
assertEquals(logs[0].properties.formattedQuery, "SELECT * FROM users");
|
|
197
|
-
assertEquals(logs[0].properties.params, []);
|
|
198
|
-
} finally {
|
|
199
|
-
await cleanup();
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("logQuery(): handles unmatched placeholders", async () => {
|
|
204
|
-
const { logs, cleanup } = await setupLogtape();
|
|
205
|
-
try {
|
|
206
|
-
const logger = getLogger();
|
|
207
|
-
logger.logQuery("SELECT * FROM users WHERE id = $1 AND name = $2", ["123"]);
|
|
208
|
-
|
|
209
|
-
assertEquals(logs.length, 1);
|
|
210
|
-
assertEquals(
|
|
211
|
-
logs[0].properties.formattedQuery,
|
|
212
|
-
"SELECT * FROM users WHERE id = '123' AND name = $2",
|
|
213
|
-
);
|
|
214
|
-
} finally {
|
|
215
|
-
await cleanup();
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("logQuery(): log message contains formatted query", async () => {
|
|
220
|
-
const { logs, cleanup } = await setupLogtape();
|
|
221
|
-
try {
|
|
222
|
-
const logger = getLogger();
|
|
223
|
-
logger.logQuery("SELECT 1", []);
|
|
224
|
-
|
|
225
|
-
assertEquals(logs.length, 1);
|
|
226
|
-
assertEquals(logs[0].rawMessage, "Query: {formattedQuery}");
|
|
227
|
-
} finally {
|
|
228
|
-
await cleanup();
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ============================================
|
|
233
|
-
// Parameter Serialization Tests
|
|
234
|
-
// ============================================
|
|
235
|
-
|
|
236
|
-
test("serialize(): handles null", () => {
|
|
237
|
-
assertEquals(serialize(null), "NULL");
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test("serialize(): handles undefined", () => {
|
|
241
|
-
assertEquals(serialize(undefined), "NULL");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test("serialize(): handles strings", () => {
|
|
245
|
-
assertEquals(serialize("hello"), "'hello'");
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test("serialize(): handles strings with special characters", () => {
|
|
249
|
-
assertEquals(serialize("hello\nworld"), "E'hello\\nworld'");
|
|
250
|
-
assertEquals(serialize("it's"), "E'it\\'s'");
|
|
251
|
-
assertEquals(serialize("back\\slash"), "E'back\\\\slash'");
|
|
252
|
-
assertEquals(serialize("tab\there"), "E'tab\\there'");
|
|
253
|
-
assertEquals(serialize("carriage\rreturn"), "E'carriage\\rreturn'");
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test("serialize(): handles numbers", () => {
|
|
257
|
-
assertEquals(serialize(42), "42");
|
|
258
|
-
assertEquals(serialize(3.14), "3.14");
|
|
259
|
-
assertEquals(serialize(-10), "-10");
|
|
260
|
-
assertEquals(serialize(0), "0");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("serialize(): handles bigints", () => {
|
|
264
|
-
assertEquals(serialize(BigInt(9007199254740991)), "9007199254740991");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test("serialize(): handles booleans", () => {
|
|
268
|
-
assertEquals(serialize(true), "'t'");
|
|
269
|
-
assertEquals(serialize(false), "'f'");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("serialize(): handles Date objects", () => {
|
|
273
|
-
const date = new Date("2024-01-15T10:30:00.000Z");
|
|
274
|
-
assertEquals(serialize(date), "'2024-01-15T10:30:00.000Z'");
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
test("serialize(): handles arrays", () => {
|
|
278
|
-
assertEquals(serialize([1, 2, 3]), "ARRAY[1, 2, 3]");
|
|
279
|
-
assertEquals(serialize(["a", "b"]), "ARRAY['a', 'b']");
|
|
280
|
-
assertEquals(serialize([]), "ARRAY[]");
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("serialize(): handles nested arrays", () => {
|
|
284
|
-
assertEquals(serialize([[1, 2], [3, 4]]), "ARRAY[ARRAY[1, 2], ARRAY[3, 4]]");
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("serialize(): handles objects as JSON", () => {
|
|
288
|
-
assertEquals(serialize({ key: "value" }), '\'{"key":"value"}\'');
|
|
289
|
-
assertEquals(serialize({ nested: { a: 1 } }), '\'{"nested":{"a":1}}\'');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("serialize(): handles mixed arrays", () => {
|
|
293
|
-
assertEquals(serialize([1, "two", true]), "ARRAY[1, 'two', 't']");
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// ============================================
|
|
297
|
-
// String Literal Tests
|
|
298
|
-
// ============================================
|
|
299
|
-
|
|
300
|
-
test("stringLiteral(): wraps simple strings", () => {
|
|
301
|
-
assertEquals(stringLiteral("hello"), "'hello'");
|
|
302
|
-
assertEquals(stringLiteral("world"), "'world'");
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("stringLiteral(): escapes single quotes", () => {
|
|
306
|
-
assertEquals(stringLiteral("it's"), "E'it\\'s'");
|
|
307
|
-
assertEquals(stringLiteral("don't"), "E'don\\'t'");
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test("stringLiteral(): escapes backslashes", () => {
|
|
311
|
-
assertEquals(stringLiteral("back\\slash"), "E'back\\\\slash'");
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test("stringLiteral(): escapes newlines", () => {
|
|
315
|
-
assertEquals(stringLiteral("line1\nline2"), "E'line1\\nline2'");
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("stringLiteral(): escapes carriage returns", () => {
|
|
319
|
-
assertEquals(stringLiteral("line1\rline2"), "E'line1\\rline2'");
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test("stringLiteral(): escapes tabs", () => {
|
|
323
|
-
assertEquals(stringLiteral("col1\tcol2"), "E'col1\\tcol2'");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("stringLiteral(): escapes multiple special characters", () => {
|
|
327
|
-
assertEquals(
|
|
328
|
-
stringLiteral("it's\na\ttest\\"),
|
|
329
|
-
"E'it\\'s\\na\\ttest\\\\'",
|
|
330
|
-
);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test("stringLiteral(): handles empty string", () => {
|
|
334
|
-
assertEquals(stringLiteral(""), "''");
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// ============================================
|
|
338
|
-
// DrizzleLogger Class Tests
|
|
339
|
-
// ============================================
|
|
340
|
-
|
|
341
|
-
test("DrizzleLogger: can be instantiated directly", async () => {
|
|
342
|
-
const { logs, cleanup } = await setupLogtape();
|
|
343
|
-
try {
|
|
344
|
-
const { getLogger: getLogtapeLogger } = await import("@logtape/logtape");
|
|
345
|
-
const logtapeLogger = getLogtapeLogger(["custom"]);
|
|
346
|
-
const drizzleLogger = new DrizzleLogger(logtapeLogger, "info");
|
|
347
|
-
|
|
348
|
-
drizzleLogger.logQuery("SELECT 1", []);
|
|
349
|
-
|
|
350
|
-
assertEquals(logs.length, 1);
|
|
351
|
-
assertEquals(logs[0].category, ["custom"]);
|
|
352
|
-
assertEquals(logs[0].level, "info");
|
|
353
|
-
} finally {
|
|
354
|
-
await cleanup();
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// ============================================
|
|
359
|
-
// Complex Query Tests
|
|
360
|
-
// ============================================
|
|
361
|
-
|
|
362
|
-
test("logQuery(): handles INSERT with multiple values", async () => {
|
|
363
|
-
const { logs, cleanup } = await setupLogtape();
|
|
364
|
-
try {
|
|
365
|
-
const logger = getLogger();
|
|
366
|
-
logger.logQuery(
|
|
367
|
-
"INSERT INTO users (name, email, age) VALUES ($1, $2, $3)",
|
|
368
|
-
["John Doe", "john@example.com", 30],
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
assertEquals(logs.length, 1);
|
|
372
|
-
assertEquals(
|
|
373
|
-
logs[0].properties.formattedQuery,
|
|
374
|
-
"INSERT INTO users (name, email, age) VALUES ('John Doe', 'john@example.com', 30)",
|
|
375
|
-
);
|
|
376
|
-
} finally {
|
|
377
|
-
await cleanup();
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("logQuery(): handles UPDATE with JSON data", async () => {
|
|
382
|
-
const { logs, cleanup } = await setupLogtape();
|
|
383
|
-
try {
|
|
384
|
-
const logger = getLogger();
|
|
385
|
-
logger.logQuery(
|
|
386
|
-
"UPDATE users SET metadata = $1 WHERE id = $2",
|
|
387
|
-
[{ role: "admin", permissions: ["read", "write"] }, 1],
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
assertEquals(logs.length, 1);
|
|
391
|
-
assertEquals(
|
|
392
|
-
logs[0].properties.formattedQuery,
|
|
393
|
-
'UPDATE users SET metadata = \'{"role":"admin","permissions":["read","write"]}\' WHERE id = 1',
|
|
394
|
-
);
|
|
395
|
-
} finally {
|
|
396
|
-
await cleanup();
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
test("logQuery(): handles DELETE query", async () => {
|
|
401
|
-
const { logs, cleanup } = await setupLogtape();
|
|
402
|
-
try {
|
|
403
|
-
const logger = getLogger();
|
|
404
|
-
logger.logQuery(
|
|
405
|
-
"DELETE FROM sessions WHERE expires_at < $1",
|
|
406
|
-
[new Date("2024-01-01T00:00:00.000Z")],
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
assertEquals(logs.length, 1);
|
|
410
|
-
assertEquals(
|
|
411
|
-
logs[0].properties.formattedQuery,
|
|
412
|
-
"DELETE FROM sessions WHERE expires_at < '2024-01-01T00:00:00.000Z'",
|
|
413
|
-
);
|
|
414
|
-
} finally {
|
|
415
|
-
await cleanup();
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
test("logQuery(): handles array parameter for IN clause", async () => {
|
|
420
|
-
const { logs, cleanup } = await setupLogtape();
|
|
421
|
-
try {
|
|
422
|
-
const logger = getLogger();
|
|
423
|
-
logger.logQuery(
|
|
424
|
-
"SELECT * FROM users WHERE id = ANY($1)",
|
|
425
|
-
[[1, 2, 3]],
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
assertEquals(logs.length, 1);
|
|
429
|
-
assertEquals(
|
|
430
|
-
logs[0].properties.formattedQuery,
|
|
431
|
-
"SELECT * FROM users WHERE id = ANY(ARRAY[1, 2, 3])",
|
|
432
|
-
);
|
|
433
|
-
} finally {
|
|
434
|
-
await cleanup();
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
test("logQuery(): handles null parameter", async () => {
|
|
439
|
-
const { logs, cleanup } = await setupLogtape();
|
|
440
|
-
try {
|
|
441
|
-
const logger = getLogger();
|
|
442
|
-
logger.logQuery(
|
|
443
|
-
"UPDATE users SET deleted_at = $1 WHERE id = $2",
|
|
444
|
-
[null, 1],
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
assertEquals(logs.length, 1);
|
|
448
|
-
assertEquals(
|
|
449
|
-
logs[0].properties.formattedQuery,
|
|
450
|
-
"UPDATE users SET deleted_at = NULL WHERE id = 1",
|
|
451
|
-
);
|
|
452
|
-
} finally {
|
|
453
|
-
await cleanup();
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
test("logQuery(): handles boolean parameters", async () => {
|
|
458
|
-
const { logs, cleanup } = await setupLogtape();
|
|
459
|
-
try {
|
|
460
|
-
const logger = getLogger();
|
|
461
|
-
logger.logQuery(
|
|
462
|
-
"SELECT * FROM users WHERE active = $1 AND verified = $2",
|
|
463
|
-
[true, false],
|
|
464
|
-
);
|
|
465
|
-
|
|
466
|
-
assertEquals(logs.length, 1);
|
|
467
|
-
assertEquals(
|
|
468
|
-
logs[0].properties.formattedQuery,
|
|
469
|
-
"SELECT * FROM users WHERE active = 't' AND verified = 'f'",
|
|
470
|
-
);
|
|
471
|
-
} finally {
|
|
472
|
-
await cleanup();
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// ============================================
|
|
477
|
-
// Edge Cases
|
|
478
|
-
// ============================================
|
|
479
|
-
|
|
480
|
-
test("logQuery(): handles empty string parameter", async () => {
|
|
481
|
-
const { logs, cleanup } = await setupLogtape();
|
|
482
|
-
try {
|
|
483
|
-
const logger = getLogger();
|
|
484
|
-
logger.logQuery("INSERT INTO users (name) VALUES ($1)", [""]);
|
|
485
|
-
|
|
486
|
-
assertEquals(logs.length, 1);
|
|
487
|
-
assertEquals(
|
|
488
|
-
logs[0].properties.formattedQuery,
|
|
489
|
-
"INSERT INTO users (name) VALUES ('')",
|
|
490
|
-
);
|
|
491
|
-
} finally {
|
|
492
|
-
await cleanup();
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
test("logQuery(): handles very long query", async () => {
|
|
497
|
-
const { logs, cleanup } = await setupLogtape();
|
|
498
|
-
try {
|
|
499
|
-
const logger = getLogger();
|
|
500
|
-
const longValue = "x".repeat(10000);
|
|
501
|
-
logger.logQuery("SELECT * FROM users WHERE data = $1", [longValue]);
|
|
502
|
-
|
|
503
|
-
assertEquals(logs.length, 1);
|
|
504
|
-
assertEquals(logs[0].properties.params, [longValue]);
|
|
505
|
-
assertEquals(
|
|
506
|
-
logs[0].properties.formattedQuery,
|
|
507
|
-
`SELECT * FROM users WHERE data = '${longValue}'`,
|
|
508
|
-
);
|
|
509
|
-
} finally {
|
|
510
|
-
await cleanup();
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test("logQuery(): handles multiple consecutive calls", async () => {
|
|
515
|
-
const { logs, cleanup } = await setupLogtape();
|
|
516
|
-
try {
|
|
517
|
-
const logger = getLogger();
|
|
518
|
-
logger.logQuery("SELECT 1", []);
|
|
519
|
-
logger.logQuery("SELECT 2", []);
|
|
520
|
-
logger.logQuery("SELECT 3", []);
|
|
521
|
-
|
|
522
|
-
assertEquals(logs.length, 3);
|
|
523
|
-
assertEquals(logs[0].properties.formattedQuery, "SELECT 1");
|
|
524
|
-
assertEquals(logs[1].properties.formattedQuery, "SELECT 2");
|
|
525
|
-
assertEquals(logs[2].properties.formattedQuery, "SELECT 3");
|
|
526
|
-
} finally {
|
|
527
|
-
await cleanup();
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
test("logQuery(): handles high placeholder numbers", async () => {
|
|
532
|
-
const { logs, cleanup } = await setupLogtape();
|
|
533
|
-
try {
|
|
534
|
-
const logger = getLogger();
|
|
535
|
-
// Build an array of 100 items: index 0="a", index 9="j", index 99="hundred"
|
|
536
|
-
const params = Array(100).fill("x");
|
|
537
|
-
params[0] = "a"; // $1
|
|
538
|
-
params[9] = "j"; // $10
|
|
539
|
-
params[99] = "hundred"; // $100
|
|
540
|
-
logger.logQuery(
|
|
541
|
-
"SELECT * FROM t WHERE a=$1 AND b=$10 AND c=$100",
|
|
542
|
-
params,
|
|
543
|
-
);
|
|
544
|
-
|
|
545
|
-
assertEquals(logs.length, 1);
|
|
546
|
-
assertEquals(
|
|
547
|
-
logs[0].properties.formattedQuery,
|
|
548
|
-
"SELECT * FROM t WHERE a='a' AND b='j' AND c='hundred'",
|
|
549
|
-
);
|
|
550
|
-
} finally {
|
|
551
|
-
await cleanup();
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
test("serialize(): handles special number values", () => {
|
|
556
|
-
assertEquals(serialize(Infinity), "Infinity");
|
|
557
|
-
assertEquals(serialize(-Infinity), "-Infinity");
|
|
558
|
-
assertEquals(serialize(NaN), "NaN");
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test("serialize(): handles negative bigint", () => {
|
|
562
|
-
assertEquals(serialize(BigInt(-9007199254740991)), "-9007199254740991");
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
test("serialize(): handles deeply nested objects", () => {
|
|
566
|
-
const nested = { a: { b: { c: { d: 1 } } } };
|
|
567
|
-
assertEquals(serialize(nested), '\'{"a":{"b":{"c":{"d":1}}}}\'');
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
test("serialize(): handles array with null and undefined", () => {
|
|
571
|
-
assertEquals(serialize([1, null, undefined, 2]), "ARRAY[1, NULL, NULL, 2]");
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
test("stringLiteral(): handles backspace and form feed", () => {
|
|
575
|
-
assertEquals(stringLiteral("a\bb"), "E'a\\bb'");
|
|
576
|
-
assertEquals(stringLiteral("a\fb"), "E'a\\fb'");
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// ============================================
|
|
580
|
-
// Type Compatibility Tests
|
|
581
|
-
// ============================================
|
|
582
|
-
|
|
583
|
-
test("getLogger(): returns Drizzle Logger interface compatible object", async () => {
|
|
584
|
-
const { cleanup } = await setupLogtape();
|
|
585
|
-
try {
|
|
586
|
-
const logger = getLogger();
|
|
587
|
-
|
|
588
|
-
// Drizzle's Logger interface only requires logQuery method
|
|
589
|
-
// Verify it matches the expected signature
|
|
590
|
-
const drizzleLogger: { logQuery(query: string, params: unknown[]): void } =
|
|
591
|
-
logger;
|
|
592
|
-
|
|
593
|
-
assertEquals(typeof drizzleLogger.logQuery, "function");
|
|
594
|
-
|
|
595
|
-
// Verify it can be called with the expected arguments
|
|
596
|
-
drizzleLogger.logQuery("SELECT 1", []);
|
|
597
|
-
drizzleLogger.logQuery("SELECT $1", ["test"]);
|
|
598
|
-
drizzleLogger.logQuery("SELECT $1, $2", [1, "two"]);
|
|
599
|
-
} finally {
|
|
600
|
-
await cleanup();
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test("Logger interface: logQuery returns void", async () => {
|
|
605
|
-
const { cleanup } = await setupLogtape();
|
|
606
|
-
try {
|
|
607
|
-
const logger = getLogger();
|
|
608
|
-
const result = logger.logQuery("SELECT 1", []);
|
|
609
|
-
|
|
610
|
-
assertEquals(result, undefined);
|
|
611
|
-
} finally {
|
|
612
|
-
await cleanup();
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
// ============================================
|
|
617
|
-
// Readonly Category Tests
|
|
618
|
-
// ============================================
|
|
619
|
-
|
|
620
|
-
test("getLogger(): handles readonly string array category", async () => {
|
|
621
|
-
const { logs, cleanup } = await setupLogtape();
|
|
622
|
-
try {
|
|
623
|
-
const category = ["app", "db"] as const;
|
|
624
|
-
const logger = getLogger({ category });
|
|
625
|
-
logger.logQuery("SELECT 1", []);
|
|
626
|
-
|
|
627
|
-
assertEquals(logs.length, 1);
|
|
628
|
-
assertEquals(logs[0].category, ["app", "db"]);
|
|
629
|
-
} finally {
|
|
630
|
-
await cleanup();
|
|
631
|
-
}
|
|
632
|
-
});
|
package/src/mod.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getLogger as getLogTapeLogger,
|
|
3
|
-
type Logger as LogTapeLogger,
|
|
4
|
-
type LogLevel,
|
|
5
|
-
} from "@logtape/logtape";
|
|
6
|
-
|
|
7
|
-
export type { LogLevel } from "@logtape/logtape";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Options for configuring the Drizzle ORM LogTape logger.
|
|
11
|
-
* @since 1.3.0
|
|
12
|
-
*/
|
|
13
|
-
export interface DrizzleLoggerOptions {
|
|
14
|
-
/**
|
|
15
|
-
* The LogTape category to use for logging.
|
|
16
|
-
* @default ["drizzle-orm"]
|
|
17
|
-
*/
|
|
18
|
-
readonly category?: string | readonly string[];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The log level to use for query logging.
|
|
22
|
-
* @default "debug"
|
|
23
|
-
*/
|
|
24
|
-
readonly level?: LogLevel;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Drizzle ORM's Logger interface.
|
|
29
|
-
* @since 1.3.0
|
|
30
|
-
*/
|
|
31
|
-
export interface Logger {
|
|
32
|
-
logQuery(query: string, params: unknown[]): void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* A Drizzle ORM-compatible logger that wraps LogTape.
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```typescript
|
|
40
|
-
* import { drizzle } from "drizzle-orm/postgres-js";
|
|
41
|
-
* import { getLogger } from "@logtape/drizzle-orm";
|
|
42
|
-
* import postgres from "postgres";
|
|
43
|
-
*
|
|
44
|
-
* const client = postgres(process.env.DATABASE_URL!);
|
|
45
|
-
* const db = drizzle(client, {
|
|
46
|
-
* logger: getLogger(),
|
|
47
|
-
* });
|
|
48
|
-
* ```
|
|
49
|
-
*
|
|
50
|
-
* @since 1.3.0
|
|
51
|
-
*/
|
|
52
|
-
export class DrizzleLogger implements Logger {
|
|
53
|
-
readonly #logger: LogTapeLogger;
|
|
54
|
-
readonly #level: LogLevel;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Creates a new DrizzleLogger instance.
|
|
58
|
-
* @param logger The LogTape logger to use.
|
|
59
|
-
* @param level The log level to use for query logging.
|
|
60
|
-
*/
|
|
61
|
-
constructor(logger: LogTapeLogger, level: LogLevel = "debug") {
|
|
62
|
-
this.#logger = logger;
|
|
63
|
-
this.#level = level;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Logs a database query with its parameters.
|
|
68
|
-
*
|
|
69
|
-
* The log output includes:
|
|
70
|
-
* - `formattedQuery`: The query with parameter placeholders replaced with
|
|
71
|
-
* actual values (for readability)
|
|
72
|
-
* - `query`: The original query string with placeholders
|
|
73
|
-
* - `params`: The original parameters array
|
|
74
|
-
*
|
|
75
|
-
* @param query The SQL query string with parameter placeholders.
|
|
76
|
-
* @param params The parameter values.
|
|
77
|
-
*/
|
|
78
|
-
logQuery(query: string, params: unknown[]): void {
|
|
79
|
-
const stringifiedParams = params.map(serialize);
|
|
80
|
-
const formattedQuery = query.replace(/\$(\d+)/g, (match) => {
|
|
81
|
-
const index = Number.parseInt(match.slice(1), 10);
|
|
82
|
-
return stringifiedParams[index - 1] ?? match;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const logMethod = this.#logger[this.#level].bind(this.#logger);
|
|
86
|
-
logMethod("Query: {formattedQuery}", {
|
|
87
|
-
formattedQuery,
|
|
88
|
-
query,
|
|
89
|
-
params,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Serializes a parameter value to a SQL-safe string representation.
|
|
96
|
-
*
|
|
97
|
-
* @param value The value to serialize.
|
|
98
|
-
* @returns The serialized string representation.
|
|
99
|
-
* @since 1.3.0
|
|
100
|
-
*/
|
|
101
|
-
export function serialize(value: unknown): string {
|
|
102
|
-
if (typeof value === "undefined" || value === null) return "NULL";
|
|
103
|
-
if (typeof value === "string") return stringLiteral(value);
|
|
104
|
-
if (typeof value === "number" || typeof value === "bigint") {
|
|
105
|
-
return value.toString();
|
|
106
|
-
}
|
|
107
|
-
if (typeof value === "boolean") return value ? "'t'" : "'f'";
|
|
108
|
-
if (value instanceof Date) return stringLiteral(value.toISOString());
|
|
109
|
-
if (Array.isArray(value)) {
|
|
110
|
-
return `ARRAY[${value.map(serialize).join(", ")}]`;
|
|
111
|
-
}
|
|
112
|
-
if (typeof value === "object") {
|
|
113
|
-
// Assume it's a JSON object
|
|
114
|
-
return stringLiteral(JSON.stringify(value));
|
|
115
|
-
}
|
|
116
|
-
return stringLiteral(String(value));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Converts a string to a SQL string literal with proper escaping.
|
|
121
|
-
*
|
|
122
|
-
* @param str The string to convert.
|
|
123
|
-
* @returns The escaped SQL string literal.
|
|
124
|
-
* @since 1.3.0
|
|
125
|
-
*/
|
|
126
|
-
export function stringLiteral(str: string): string {
|
|
127
|
-
if (/[\\'\n\r\t\b\f]/.test(str)) {
|
|
128
|
-
let escaped = str;
|
|
129
|
-
escaped = escaped.replaceAll("\\", "\\\\");
|
|
130
|
-
escaped = escaped.replaceAll("'", "\\'");
|
|
131
|
-
escaped = escaped.replaceAll("\n", "\\n");
|
|
132
|
-
escaped = escaped.replaceAll("\r", "\\r");
|
|
133
|
-
escaped = escaped.replaceAll("\t", "\\t");
|
|
134
|
-
escaped = escaped.replaceAll("\b", "\\b");
|
|
135
|
-
escaped = escaped.replaceAll("\f", "\\f");
|
|
136
|
-
return `E'${escaped}'`;
|
|
137
|
-
}
|
|
138
|
-
return `'${str}'`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Normalize category to array format.
|
|
143
|
-
*/
|
|
144
|
-
function normalizeCategory(
|
|
145
|
-
category: string | readonly string[],
|
|
146
|
-
): readonly string[] {
|
|
147
|
-
return typeof category === "string" ? [category] : category;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Creates a Drizzle ORM-compatible logger that wraps LogTape.
|
|
152
|
-
*
|
|
153
|
-
* @example Basic usage
|
|
154
|
-
* ```typescript
|
|
155
|
-
* import { drizzle } from "drizzle-orm/postgres-js";
|
|
156
|
-
* import { configure } from "@logtape/logtape";
|
|
157
|
-
* import { getLogger } from "@logtape/drizzle-orm";
|
|
158
|
-
* import postgres from "postgres";
|
|
159
|
-
*
|
|
160
|
-
* await configure({
|
|
161
|
-
* // ... LogTape configuration
|
|
162
|
-
* });
|
|
163
|
-
*
|
|
164
|
-
* const client = postgres(process.env.DATABASE_URL!);
|
|
165
|
-
* const db = drizzle(client, {
|
|
166
|
-
* logger: getLogger(),
|
|
167
|
-
* });
|
|
168
|
-
* ```
|
|
169
|
-
*
|
|
170
|
-
* @example With custom category and level
|
|
171
|
-
* ```typescript
|
|
172
|
-
* const db = drizzle(client, {
|
|
173
|
-
* logger: getLogger({
|
|
174
|
-
* category: ["my-app", "database"],
|
|
175
|
-
* level: "info",
|
|
176
|
-
* }),
|
|
177
|
-
* });
|
|
178
|
-
* ```
|
|
179
|
-
*
|
|
180
|
-
* @param options Configuration options for the logger.
|
|
181
|
-
* @returns A Drizzle ORM-compatible logger wrapping LogTape.
|
|
182
|
-
* @since 1.3.0
|
|
183
|
-
*/
|
|
184
|
-
export function getLogger(options: DrizzleLoggerOptions = {}): DrizzleLogger {
|
|
185
|
-
const category = normalizeCategory(options.category ?? ["drizzle-orm"]);
|
|
186
|
-
const logger = getLogTapeLogger(category);
|
|
187
|
-
return new DrizzleLogger(logger, options.level ?? "debug");
|
|
188
|
-
}
|