@logtape/drizzle-orm 1.3.4 → 1.4.0-dev.408

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/deno.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@logtape/drizzle-orm",
3
+ "version": "1.4.0-dev.408+3fd750bb",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/drizzle-orm",
3
- "version": "1.3.4",
3
+ "version": "1.4.0-dev.408+3fd750bb",
4
4
  "description": "Drizzle ORM adapter for LogTape logging library",
5
5
  "keywords": [
6
6
  "logging",
@@ -48,11 +48,8 @@
48
48
  "./package.json": "./package.json"
49
49
  },
50
50
  "sideEffects": false,
51
- "files": [
52
- "dist/"
53
- ],
54
51
  "peerDependencies": {
55
- "@logtape/logtape": "^1.3.4"
52
+ "@logtape/logtape": "^1.4.0-dev.408+3fd750bb"
56
53
  },
57
54
  "devDependencies": {
58
55
  "@alinea/suite": "^0.6.3",
@@ -0,0 +1,632 @@
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 ADDED
@@ -0,0 +1,188 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: "src/mod.ts",
5
+ dts: {
6
+ sourcemap: true,
7
+ },
8
+ format: ["esm", "cjs"],
9
+ platform: "neutral",
10
+ unbundle: true,
11
+ });