@gallopsystems/agent-skills 1.0.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.
Files changed (52) hide show
  1. package/README.md +137 -0
  2. package/package.json +26 -0
  3. package/plugins/doctl/.claude-plugin/plugin.json +8 -0
  4. package/plugins/doctl/skills/doctl/SKILL.md +93 -0
  5. package/plugins/kysely-postgres/.claude-plugin/plugin.json +8 -0
  6. package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +1101 -0
  7. package/plugins/kysely-postgres/skills/kysely-postgres/references/aggregations.ts +167 -0
  8. package/plugins/kysely-postgres/skills/kysely-postgres/references/ctes.ts +165 -0
  9. package/plugins/kysely-postgres/skills/kysely-postgres/references/expressions.ts +272 -0
  10. package/plugins/kysely-postgres/skills/kysely-postgres/references/joins.ts +206 -0
  11. package/plugins/kysely-postgres/skills/kysely-postgres/references/json-arrays.ts +398 -0
  12. package/plugins/kysely-postgres/skills/kysely-postgres/references/mutations.ts +199 -0
  13. package/plugins/kysely-postgres/skills/kysely-postgres/references/orderby-pagination.ts +117 -0
  14. package/plugins/kysely-postgres/skills/kysely-postgres/references/relations.ts +176 -0
  15. package/plugins/kysely-postgres/skills/kysely-postgres/references/select-where.ts +146 -0
  16. package/plugins/linear/.claude-plugin/plugin.json +8 -0
  17. package/plugins/linear/skills/linear/SKILL.md +1040 -0
  18. package/plugins/linear/skills/linear/bin/linear.mjs +1228 -0
  19. package/plugins/linear/skills/linear/tech-stack.md +273 -0
  20. package/plugins/nitro-testing/.claude-plugin/plugin.json +8 -0
  21. package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +497 -0
  22. package/plugins/nitro-testing/skills/nitro-testing/async-testing.md +270 -0
  23. package/plugins/nitro-testing/skills/nitro-testing/ci-setup.md +226 -0
  24. package/plugins/nitro-testing/skills/nitro-testing/examples/global-setup.ts +90 -0
  25. package/plugins/nitro-testing/skills/nitro-testing/examples/handler.test.ts +167 -0
  26. package/plugins/nitro-testing/skills/nitro-testing/examples/setup.ts +29 -0
  27. package/plugins/nitro-testing/skills/nitro-testing/examples/test-utils-index.ts +297 -0
  28. package/plugins/nitro-testing/skills/nitro-testing/examples/vitest.config.ts +42 -0
  29. package/plugins/nitro-testing/skills/nitro-testing/factories.md +278 -0
  30. package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +512 -0
  31. package/plugins/nitro-testing/skills/nitro-testing/test-utils.md +262 -0
  32. package/plugins/nitro-testing/skills/nitro-testing/transaction-rollback.md +183 -0
  33. package/plugins/nitro-testing/skills/nitro-testing/vitest-config.md +236 -0
  34. package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +8 -0
  35. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +260 -0
  36. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +228 -0
  37. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +174 -0
  38. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/deep-linking.md +190 -0
  39. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-middleware.ts +32 -0
  40. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-utils.ts +51 -0
  41. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/deep-link-page.vue +61 -0
  42. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/service-util.ts +63 -0
  43. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/sse-endpoint.ts +59 -0
  44. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/validation-endpoint.ts +38 -0
  45. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +178 -0
  46. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/nitro-tasks.md +243 -0
  47. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +162 -0
  48. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-services.md +238 -0
  49. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/sse.md +221 -0
  50. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +166 -0
  51. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/validation.md +131 -0
  52. package/scripts/link-skills.mjs +252 -0
@@ -0,0 +1,1101 @@
1
+ ---
2
+ name: kysely-postgres
3
+ description: Write effective, type-safe Kysely queries for PostgreSQL. This skill should be used when working in Node.js/TypeScript backends with Kysely installed, covering query patterns, migrations, type generation, and common pitfalls to avoid.
4
+ ---
5
+
6
+ # Kysely for PostgreSQL
7
+
8
+ Kysely is a type-safe TypeScript SQL query builder. This skill provides patterns for writing effective queries, managing migrations, and avoiding common pitfalls.
9
+
10
+ ## When to Use This Skill
11
+
12
+ Use this skill when:
13
+ - Working in a Node.js/TypeScript project with Kysely installed
14
+ - Writing database queries for PostgreSQL
15
+ - Creating or modifying database migrations
16
+ - Debugging type inference issues in Kysely queries
17
+
18
+ ## Reference Files
19
+
20
+ For detailed examples, see these topic-focused reference files:
21
+
22
+ - [select-where.ts](references/select-where.ts) - Basic SELECT patterns, WHERE clauses, AND/OR conditions
23
+ - [joins.ts](references/joins.ts) - Simple joins, callback joins, subquery joins, cross joins
24
+ - [aggregations.ts](references/aggregations.ts) - COUNT, SUM, AVG, GROUP BY, HAVING
25
+ - [orderby-pagination.ts](references/orderby-pagination.ts) - ORDER BY, NULLS handling, DISTINCT, pagination
26
+ - [ctes.ts](references/ctes.ts) - Common Table Expressions, multiple CTEs, recursive CTEs
27
+ - [json-arrays.ts](references/json-arrays.ts) - JSONB handling, array columns, jsonBuildObject, jsonAgg
28
+ - [relations.ts](references/relations.ts) - jsonArrayFrom, jsonObjectFrom for nested data
29
+ - [mutations.ts](references/mutations.ts) - INSERT, UPDATE, DELETE, UPSERT, INSERT FROM SELECT
30
+ - [expressions.ts](references/expressions.ts) - CASE, $if, subqueries, eb.val/lit/not, standalone expressionBuilder
31
+
32
+ ## Core Principles
33
+
34
+ 1. **Prefer Kysely methods over raw SQL**: Almost everything you can do in SQL, you can do in Kysely without `sql``
35
+ 2. **Use the ExpressionBuilder (eb)**: The `eb` parameter in callbacks is the foundation of type-safe query building
36
+ 3. **Let TypeScript guide you**: If it compiles, it's likely correct SQL
37
+
38
+ ## ExpressionBuilder (eb) - The Foundation
39
+
40
+ The `eb` parameter in select/where callbacks provides all expression methods:
41
+
42
+ ```typescript
43
+ .select((eb) => [
44
+ eb.ref("column").as("alias"), // Column reference
45
+ eb.fn<string>("upper", [eb.ref("email")]), // Function call (typed!)
46
+ eb.fn.count("id").as("count"), // Aggregate function
47
+ eb.fn.sum("amount").as("total"), // SUM
48
+ eb.fn.avg("rating").as("avgRating"), // AVG
49
+ eb.fn.coalesce("nullable_col", eb.val(0)), // COALESCE
50
+ eb.case().when("status", "=", "active") // CASE expression
51
+ .then("Active").else("Inactive").end(),
52
+ eb("quantity", "*", eb.ref("unit_price")), // Binary expression
53
+ eb.exists(subquery), // EXISTS
54
+ eb.not(expression), // NOT / negation
55
+ eb.cast(eb.val(" "), "text"), // Cast value to type
56
+ eb.and([...]), // AND conditions
57
+ eb.or([...]), // OR conditions
58
+ ])
59
+ ```
60
+
61
+ ### eb.val() vs eb.lit()
62
+
63
+ ```typescript
64
+ // eb.val() - Creates a parameterized value ($1, $2, etc.) - PREFERRED for user input
65
+ // Note: eb.val() alone may fail with "could not determine data type of parameter"
66
+ // Use eb.cast(eb.val(...), "text") for string values in function arguments
67
+ eb.val("user input") // Becomes: $1 with parameter "user input"
68
+ eb.cast(eb.val("safe"), "text") // Becomes: $1::text - always works
69
+
70
+ // eb.lit() - Creates a literal value in SQL
71
+ // ONLY accepts: numbers, booleans, null - NOT strings (throws "unsafe immediate value")
72
+ eb.lit(1) // Becomes: 1 (directly in SQL)
73
+ eb.lit(true) // Becomes: true
74
+ eb.lit(null) // Becomes: NULL
75
+
76
+ // For string literals, use sql`` template instead
77
+ sql`'active'` // Becomes: 'active' (directly in SQL)
78
+ sql<string>`'label'` // Typed string literal
79
+ ```
80
+
81
+ ### Standalone ExpressionBuilder
82
+
83
+ For reusable helpers outside query callbacks:
84
+
85
+ ```typescript
86
+ import { expressionBuilder } from "kysely";
87
+ import type { DB } from "./db.d.ts";
88
+
89
+ // Create standalone expression builder
90
+ const eb = expressionBuilder<DB, "user">();
91
+
92
+ // Use in helper functions
93
+ function isActiveUser() {
94
+ return eb.and([
95
+ eb("is_active", "=", true),
96
+ eb("role", "!=", "banned"),
97
+ ]);
98
+ }
99
+ ```
100
+
101
+ ### Conditional Expressions with Arrays
102
+
103
+ Build dynamic filters by collecting expressions:
104
+
105
+ ```typescript
106
+ .where((eb) => {
107
+ const filters: Expression<SqlBool>[] = [];
108
+
109
+ if (firstName) filters.push(eb("first_name", "=", firstName));
110
+ if (lastName) filters.push(eb("last_name", "=", lastName));
111
+ if (minAge) filters.push(eb("age", ">=", minAge));
112
+
113
+ // Combine all filters with AND (empty array = no filter)
114
+ return eb.and(filters);
115
+ })
116
+ ```
117
+
118
+ ## String Concatenation
119
+
120
+ Use the `||` operator with `sql` template for clean string concatenation:
121
+
122
+ ```typescript
123
+ // RECOMMENDED - Clean and type-safe with eb.ref()
124
+ .select((eb) => [
125
+ sql<string>`${eb.ref("first_name")} || ' ' || ${eb.ref("last_name")}`.as("full_name"),
126
+ ])
127
+ // Output: "first_name" || ' ' || "last_name"
128
+
129
+ // ALTERNATIVE - Pure eb() chaining (parameterized literals)
130
+ .select((eb) => [
131
+ eb(eb("first_name", "||", " "), "||", eb.ref("last_name")).as("full_name"),
132
+ ])
133
+ // Output: "first_name" || $1 || "last_name"
134
+
135
+ // VERBOSE - concat() function (avoid unless you need NULL handling)
136
+ .select((eb) => [
137
+ eb.fn<string>("concat", [
138
+ eb.ref("first_name"),
139
+ eb.cast(eb.val(" "), "text"),
140
+ eb.ref("last_name"),
141
+ ]).as("full_name"),
142
+ ])
143
+ ```
144
+
145
+ **Note**: `concat()` treats NULL as empty string, while `||` propagates NULL. Use `concat()` only when you need that NULL behavior.
146
+
147
+ ## Query Patterns
148
+
149
+ ### Basic SELECT
150
+
151
+ ```typescript
152
+ // Select all columns
153
+ const users = await db.selectFrom("user").selectAll().execute();
154
+
155
+ // Select specific columns with aliases
156
+ const users = await db
157
+ .selectFrom("user")
158
+ .select(["id", "email", "first_name as firstName"])
159
+ .execute();
160
+
161
+ // Single row (returns T | undefined)
162
+ const user = await db.selectFrom("user").selectAll()
163
+ .where("id", "=", userId).executeTakeFirst();
164
+
165
+ // Single row that must exist (throws if not found)
166
+ const user = await db.selectFrom("user").selectAll()
167
+ .where("id", "=", userId).executeTakeFirstOrThrow();
168
+ ```
169
+
170
+ ### WHERE Clauses
171
+
172
+ ```typescript
173
+ // Equality, comparison, IN, LIKE
174
+ .where("status", "=", "active")
175
+ .where("price", ">", 100)
176
+ .where("role", "in", ["admin", "manager"])
177
+ .where("name", "like", "%search%")
178
+ .where("deleted_at", "is", null)
179
+
180
+ // Multiple conditions (chained = AND)
181
+ .where("is_active", "=", true)
182
+ .where("role", "=", "admin")
183
+
184
+ // OR conditions
185
+ .where((eb) => eb.or([
186
+ eb("role", "=", "admin"),
187
+ eb("role", "=", "manager"),
188
+ ]))
189
+
190
+ // Complex AND/OR
191
+ .where((eb) => eb.and([
192
+ eb("is_active", "=", true),
193
+ eb.or([
194
+ eb("price", "<", 50),
195
+ eb("stock", ">", 100),
196
+ ]),
197
+ ]))
198
+ ```
199
+
200
+ ### JOINs
201
+
202
+ ```typescript
203
+ // Inner join
204
+ .innerJoin("order", "order.user_id", "user.id")
205
+
206
+ // Left join
207
+ .leftJoin("category", "category.id", "product.category_id")
208
+
209
+ // Self-join with alias
210
+ .selectFrom("category as c")
211
+ .leftJoin("category as parent", "parent.id", "c.parent_id")
212
+
213
+ // Multiple joins
214
+ .innerJoin("order", "order.id", "order_item.order_id")
215
+ .innerJoin("product", "product.id", "order_item.product_id")
216
+ .innerJoin("user", "user.id", "order.user_id")
217
+ ```
218
+
219
+ ### Complex JOINs (Callback Format)
220
+
221
+ Use the callback format when you need:
222
+ - Multiple join conditions (composite keys)
223
+ - Mixed column-to-column and column-to-literal comparisons
224
+ - OR conditions within joins
225
+ - Subquery joins (derived tables)
226
+
227
+ **Join Builder Methods:**
228
+ - `onRef(col1, op, col2)` - Column-to-column comparison
229
+ - `on(col, op, value)` - Column-to-literal comparison
230
+ - `on((eb) => ...)` - Complex expressions with OR logic
231
+
232
+ ```typescript
233
+ // Multi-condition join (composite key + filter)
234
+ .leftJoin("invoice as i", (join) =>
235
+ join
236
+ .onRef("sp.service_provider_id", "=", "i.service_provider_id")
237
+ .onRef("sp.year", "=", "i.year")
238
+ .onRef("sp.month", "=", "i.month")
239
+ .on("i.status", "!=", "invalidated")
240
+ )
241
+
242
+ // Join with OR conditions
243
+ .leftJoin("order as o", (join) =>
244
+ join
245
+ .onRef("o.user_id", "=", "u.id")
246
+ .on((eb) =>
247
+ eb.or([
248
+ eb("o.status", "=", "completed"),
249
+ eb("o.status", "=", "shipped"),
250
+ ])
251
+ )
252
+ )
253
+
254
+ // Subquery join (derived table) - two callbacks
255
+ .leftJoin(
256
+ (eb) =>
257
+ eb
258
+ .selectFrom("order")
259
+ .select((eb) => [
260
+ "user_id",
261
+ eb.fn.count("id").as("order_count"),
262
+ eb.fn.max("created_at").as("last_order_at"),
263
+ ])
264
+ .groupBy("user_id")
265
+ .as("order_stats"), // MUST have alias!
266
+ (join) => join.onRef("order_stats.user_id", "=", "u.id")
267
+ )
268
+
269
+ // Cross join (always-true condition) - for joining aggregated CTEs
270
+ .leftJoin("summary_cte", (join) =>
271
+ join.on(sql`true`, "=", sql`true`)
272
+ )
273
+ ```
274
+
275
+ ### Aggregations
276
+
277
+ ```typescript
278
+ .select((eb) => [
279
+ "status",
280
+ eb.fn.count("id").as("count"),
281
+ eb.fn.sum("total_amount").as("totalAmount"),
282
+ eb.fn.avg("total_amount").as("avgAmount"),
283
+ ])
284
+ .groupBy("status")
285
+ .having((eb) => eb.fn.count("id"), ">", 5)
286
+ ```
287
+
288
+ #### FILTER (WHERE ...) on Aggregates
289
+
290
+ PostgreSQL's `FILTER (WHERE ...)` clause is available on **all** aggregate function builders via `.filterWhere()`:
291
+
292
+ ```typescript
293
+ .select((eb) => [
294
+ eb.fn.count("id").filterWhere("status", "=", "active").as("active_count"),
295
+ eb.fn.countAll().filterWhere("role", "!=", "banned").as("non_banned"),
296
+ eb.fn.sum("amount").filterWhere("type", "=", "credit").as("total_credits"),
297
+ ])
298
+
299
+ // Also works as the first argument to .having()
300
+ .having(
301
+ (eb) => eb.fn.countAll().filterWhere("status", "!=", "signed"),
302
+ "=",
303
+ 0
304
+ )
305
+ ```
306
+
307
+ ### ORDER BY
308
+
309
+ ```typescript
310
+ // Simple ordering
311
+ .orderBy("created_at", "desc")
312
+ .orderBy("name", "asc")
313
+
314
+ // NULLS FIRST / NULLS LAST - use order builder callback
315
+ .orderBy("category_id", (ob) => ob.asc().nullsLast())
316
+ .orderBy("priority", (ob) => ob.desc().nullsFirst())
317
+
318
+ // Multiple columns - chain orderBy calls (array syntax is deprecated)
319
+ .orderBy("category_id", "asc")
320
+ .orderBy("price", "desc")
321
+ .orderBy("name", "asc")
322
+ ```
323
+
324
+ ### CTEs (Common Table Expressions)
325
+
326
+ Use CTEs for complex queries with multiple aggregation levels:
327
+
328
+ ```typescript
329
+ const result = await db
330
+ .with("order_totals", (db) =>
331
+ db.selectFrom("order")
332
+ .innerJoin("user", "user.id", "order.user_id")
333
+ .select((eb) => [
334
+ "user.id as userId",
335
+ "user.email",
336
+ eb.fn.sum("order.total_amount").as("totalSpent"),
337
+ eb.fn.count("order.id").as("orderCount"),
338
+ ])
339
+ .groupBy(["user.id", "user.email"])
340
+ )
341
+ .selectFrom("order_totals")
342
+ .selectAll()
343
+ .orderBy("totalSpent", "desc")
344
+ .execute();
345
+ ```
346
+
347
+ ### JSON Aggregation (PostgreSQL)
348
+
349
+ ```typescript
350
+ import { jsonBuildObject } from "kysely/helpers/postgres";
351
+ // Note: jsonAgg is accessed via eb.fn.jsonAgg(), not imported
352
+
353
+ .with("tasks", (db) =>
354
+ db.selectFrom("task")
355
+ .leftJoin("user", "user.id", "task.assignee_id")
356
+ .select((eb) => [
357
+ "task.job_id",
358
+ eb.fn.jsonAgg(
359
+ jsonBuildObject({
360
+ id: eb.ref("task.id"),
361
+ status: eb.ref("task.status"),
362
+ assignee: jsonBuildObject({
363
+ id: eb.ref("user.id"),
364
+ name: eb.fn<string>("concat", [
365
+ eb.ref("user.first_name"),
366
+ eb.cast(eb.val(" "), "text"),
367
+ eb.ref("user.last_name"),
368
+ ]),
369
+ }),
370
+ })
371
+ )
372
+ .filterWhere("task.id", "is not", null) // Filter nulls from left join
373
+ .as("tasks"),
374
+ ])
375
+ .groupBy("task.job_id")
376
+ )
377
+ ```
378
+
379
+ ## JSON, JSONB, and Array Handling
380
+
381
+ ### JSONB Columns
382
+
383
+ **NO `JSON.stringify` or `JSON.parse` needed!** The `pg` driver handles JSONB automatically:
384
+
385
+ ```typescript
386
+ // INSERT - pass objects directly
387
+ await db
388
+ .insertInto("user")
389
+ .values({
390
+ email: "test@example.com",
391
+ metadata: { preferences: { theme: "dark" }, count: 42 },
392
+ })
393
+ .execute();
394
+
395
+ // UPDATE - pass objects directly
396
+ await db
397
+ .updateTable("user")
398
+ .set({
399
+ metadata: { preferences: { theme: "light" } },
400
+ })
401
+ .where("id", "=", userId)
402
+ .execute();
403
+
404
+ // READ - returns parsed object, not string
405
+ const user = await db
406
+ .selectFrom("user")
407
+ .select(["id", "metadata"])
408
+ .executeTakeFirst();
409
+ console.log(user.metadata.preferences.theme); // "dark" - already an object!
410
+ ```
411
+
412
+ ### Array Columns (text[], int[], etc.)
413
+
414
+ **NO `JSON.stringify` needed for array columns!** The `pg` driver handles arrays natively:
415
+
416
+ ```typescript
417
+ // INSERT with array - pass array directly
418
+ await db
419
+ .insertInto("product")
420
+ .values({
421
+ name: "Product",
422
+ tags: ["phone", "electronics", "premium"], // Direct array!
423
+ })
424
+ .execute();
425
+
426
+ // READ - returns as native JavaScript array
427
+ const product = await db
428
+ .selectFrom("product")
429
+ .select(["name", "tags"])
430
+ .executeTakeFirst();
431
+ console.log(product.tags); // ["phone", "electronics", "premium"]
432
+
433
+ // UPDATE array
434
+ await db
435
+ .updateTable("product")
436
+ .set({ tags: ["updated", "tags"] })
437
+ .where("id", "=", productId)
438
+ .execute();
439
+ ```
440
+
441
+ ### Querying Arrays
442
+
443
+ ```typescript
444
+ // Array contains all values (@>) - operator works natively!
445
+ .where("tags", "@>", sql`ARRAY['phone', 'premium']::text[]`)
446
+
447
+ // Arrays overlap (&&) - operator works natively!
448
+ .where("tags", "&&", sql`ARRAY['premium', 'basic']::text[]`)
449
+
450
+ // Array contains value (ANY) - type-safe with eb.fn
451
+ .where((eb) => eb(sql`${searchTerm}`, "=", eb.fn("any", [eb.ref("tags")])))
452
+ // eb.ref("tags") validates column exists - eb.ref("invalid") would be a TS error
453
+ ```
454
+
455
+ ### Querying JSONB
456
+
457
+ ```typescript
458
+ // Key exists (?) - operator works natively!
459
+ .where("metadata", "?", "theme")
460
+
461
+ // Any key exists (?|) - operator works natively!
462
+ .where("metadata", "?|", sql`array['theme', 'language']`)
463
+
464
+ // All keys exist (?&) - operator works natively!
465
+ .where("metadata", "?&", sql`array['theme', 'notifications']`)
466
+
467
+ // JSONB contains (@>) - operator works natively!
468
+ .where("metadata", "@>", sql`'{"notifications": true}'::jsonb`)
469
+
470
+ // Extract field as text (->> as operator) - type-safe!
471
+ .where((eb) => eb(eb("metadata", "->>", "theme"), "=", "dark"))
472
+ // eb("metadata", ...) validates column - eb("invalid", ...) would be TS error
473
+
474
+ // Extract nested path (#>> still needs sql``)
475
+ .where(sql`metadata#>>'{preferences,theme}'`, "=", "dark")
476
+
477
+ // In SELECT - type-safe with eb()
478
+ .select((eb) => [
479
+ eb("metadata", "->", "preferences").as("prefs"), // Returns JSONB
480
+ eb("metadata", "->>", "theme").as("theme"), // Returns text
481
+ ])
482
+ // Nested paths still need sql``
483
+ .select(sql`metadata#>'{preferences,theme}'`.as("t")) // Nested as JSONB
484
+ .select(sql<string>`metadata#>>'{a,b}'`.as("t")) // Nested as text
485
+ ```
486
+
487
+ ### JSONPath (PostgreSQL 12+)
488
+
489
+ ```typescript
490
+ // JSONPath match (@@) - works as native operator!
491
+ .where("metadata", "@@", sql`'$.preferences.theme == "dark"'`)
492
+
493
+ // JSONPath exists (@?) - NOT in Kysely's allowlist, use function instead
494
+ // Use jsonb_path_exists() for type-safe column validation
495
+ .where((eb) =>
496
+ eb.fn("jsonb_path_exists", [eb.ref("metadata"), sql`'$.preferences.theme'`])
497
+ )
498
+ // eb.ref("metadata") validates column - eb.ref("invalid") would be TS error
499
+
500
+ // Extract with JSONPath - type-safe with eb.fn
501
+ .select((eb) => [
502
+ "id",
503
+ eb.fn("jsonb_path_query_first", [eb.ref("metadata"), sql`'$.preferences.theme'`]).as("theme"),
504
+ ])
505
+
506
+ // JSONPath with variables
507
+ const searchValue = "dark";
508
+ .where((eb) =>
509
+ eb.fn("jsonb_path_exists", [
510
+ eb.ref("metadata"),
511
+ sql`'$.preferences.theme ? (@ == $val)'`,
512
+ sql`jsonb_build_object('val', ${searchValue}::text)`,
513
+ ])
514
+ )
515
+ ```
516
+
517
+ ### Conditional Queries ($if)
518
+
519
+ Use `$if()` for runtime-conditional query modifications:
520
+
521
+ ```typescript
522
+ const result = await db
523
+ .selectFrom("user")
524
+ .selectAll()
525
+ .$if(!includeInactive, (qb) => qb.where("is_active", "=", true))
526
+ .$if(includeMetadata, (qb) => qb.select("metadata"))
527
+ .$if(!!searchTerm, (qb) => qb.where("name", "like", `%${searchTerm}%`))
528
+ .$if(!!roleFilter, (qb) => qb.where("role", "in", roleFilter!))
529
+ .execute();
530
+ ```
531
+
532
+ **Type behavior**: Columns added via `$if` become optional in the result type since inclusion isn't guaranteed at compile time.
533
+
534
+ ### Relations (jsonArrayFrom / jsonObjectFrom)
535
+
536
+ Kysely is NOT an ORM - it uses PostgreSQL's JSON functions for nested data:
537
+
538
+ ```typescript
539
+ import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
540
+
541
+ // One-to-many: User with their orders
542
+ const users = await db
543
+ .selectFrom("user")
544
+ .select((eb) => [
545
+ "user.id",
546
+ "user.email",
547
+ jsonArrayFrom(
548
+ eb
549
+ .selectFrom("order")
550
+ .select(["order.id", "order.status", "order.total_amount"])
551
+ .whereRef("order.user_id", "=", "user.id")
552
+ .orderBy("order.created_at", "desc")
553
+ ).as("orders"),
554
+ ])
555
+ .execute();
556
+
557
+ // Many-to-one: Product with its category
558
+ const products = await db
559
+ .selectFrom("product")
560
+ .select((eb) => [
561
+ "product.id",
562
+ "product.name",
563
+ jsonObjectFrom(
564
+ eb
565
+ .selectFrom("category")
566
+ .select(["category.id", "category.name"])
567
+ .whereRef("category.id", "=", "product.category_id")
568
+ ).as("category"),
569
+ ])
570
+ .execute();
571
+ ```
572
+
573
+ **Critical: Use explicit `.select()` instead of `.selectAll()` with nested json helpers**
574
+
575
+ When using `jsonObjectFrom` containing a nested `jsonArrayFrom` (or vice versa), using `selectAll("table")` breaks TypeScript's type inference. The result type becomes `unknown` or loses the nested structure, requiring `$castTo` to fix.
576
+
577
+ ```typescript
578
+ // WRONG - selectAll() breaks type inference for nested json helpers
579
+ const invoice = await db
580
+ .selectFrom("invoices")
581
+ .selectAll("invoices")
582
+ .select((eb) => [
583
+ jsonObjectFrom(
584
+ eb
585
+ .selectFrom("payment_plans")
586
+ .selectAll() // ❌ This breaks type inference!
587
+ .select((eb2) => [
588
+ jsonArrayFrom(
589
+ eb2.selectFrom("installments").selectAll()
590
+ .whereRef("installments.plan_id", "=", "payment_plans.id")
591
+ ).as("installments"),
592
+ ])
593
+ .whereRef("payment_plans.invoice_id", "=", "invoices.id")
594
+ ).as("payment_plan"), // Type is unknown or broken
595
+ ])
596
+ .executeTakeFirst();
597
+
598
+ // RIGHT - explicit select() preserves type inference
599
+ const invoice = await db
600
+ .selectFrom("invoices")
601
+ .selectAll("invoices")
602
+ .select((eb) => [
603
+ jsonObjectFrom(
604
+ eb
605
+ .selectFrom("payment_plans")
606
+ .select([ // ✅ Explicit columns!
607
+ "payment_plans.id",
608
+ "payment_plans.invoice_id",
609
+ "payment_plans.notes",
610
+ "payment_plans.created_at",
611
+ ])
612
+ .select((eb2) => [
613
+ jsonArrayFrom(
614
+ eb2.selectFrom("installments").selectAll()
615
+ .whereRef("installments.plan_id", "=", "payment_plans.id")
616
+ ).as("installments"),
617
+ ])
618
+ .whereRef("payment_plans.invoice_id", "=", "invoices.id")
619
+ ).as("payment_plan"), // Type is properly inferred!
620
+ ])
621
+ .executeTakeFirst();
622
+ ```
623
+
624
+ **Why this happens**: Kysely's type inference for nested json helpers relies on tracking the selected columns through the query chain. `selectAll()` returns all columns dynamically, which confuses TypeScript when combined with additional `.select()` calls that add nested json helpers. Using explicit column names gives TypeScript the static information it needs.
625
+
626
+ **Rule of thumb**: When combining `jsonObjectFrom`/`jsonArrayFrom` with nested json helpers, always use explicit `.select([...columns])` instead of `.selectAll()` on the subquery containing the nested helper.
627
+
628
+ ### Reusable Helpers
629
+
630
+ Create composable, type-safe helper functions using `Expression<T>`:
631
+
632
+ ```typescript
633
+ import { Expression, sql } from "kysely";
634
+
635
+ // Helper that takes and returns Expression<string>
636
+ function lower(expr: Expression<string>) {
637
+ return sql<string>`lower(${expr})`;
638
+ }
639
+
640
+ // Use in queries
641
+ .where(({ eb, ref }) => eb(lower(ref("email")), "=", email.toLowerCase()))
642
+ ```
643
+
644
+ ### Splitting Query Building and Execution
645
+
646
+ Build queries without executing, useful for dynamic query construction:
647
+
648
+ ```typescript
649
+ // Build query (doesn't execute)
650
+ let query = db
651
+ .selectFrom("user")
652
+ .select(["id", "email"]);
653
+
654
+ // Add conditions dynamically
655
+ if (role) {
656
+ query = query.where("role", "=", role);
657
+ }
658
+ if (isActive !== undefined) {
659
+ query = query.where("is_active", "=", isActive);
660
+ }
661
+
662
+ // Execute when ready
663
+ const results = await query.execute();
664
+
665
+ // Or compile to SQL without executing
666
+ const compiled = query.compile();
667
+ console.log(compiled.sql); // The SQL string
668
+ console.log(compiled.parameters); // Bound parameters
669
+ ```
670
+
671
+ ### Subqueries
672
+
673
+ ```typescript
674
+ // Subquery in WHERE
675
+ .where("id", "in",
676
+ db.selectFrom("order").select("user_id").where("status", "=", "completed")
677
+ )
678
+
679
+ // EXISTS subquery
680
+ .where((eb) =>
681
+ eb.exists(
682
+ db.selectFrom("review")
683
+ .select(sql`1`.as("one"))
684
+ .whereRef("review.product_id", "=", eb.ref("product.id"))
685
+ )
686
+ )
687
+ ```
688
+
689
+ ### INSERT Operations
690
+
691
+ ```typescript
692
+ // Single insert with returning
693
+ const user = await db
694
+ .insertInto("user")
695
+ .values({ email: "test@example.com", first_name: "Test", last_name: "User" })
696
+ .returning(["id", "email"])
697
+ .executeTakeFirst();
698
+
699
+ // Multiple rows
700
+ await db
701
+ .insertInto("user")
702
+ .values([
703
+ { email: "a@example.com", first_name: "A", last_name: "User" },
704
+ { email: "b@example.com", first_name: "B", last_name: "User" },
705
+ ])
706
+ .execute();
707
+
708
+ // Upsert (ON CONFLICT) - type-safe with expression builder
709
+ await db
710
+ .insertInto("product")
711
+ .values({ sku: "ABC123", name: "Product", stock_quantity: 10 })
712
+ .onConflict((oc) =>
713
+ oc.column("sku").doUpdateSet((eb) => ({
714
+ stock_quantity: eb("product.stock_quantity", "+", eb.ref("excluded.stock_quantity")),
715
+ }))
716
+ )
717
+ .execute();
718
+ // eb("product.invalid_column", ...) would be a TypeScript error!
719
+
720
+ // Insert from SELECT
721
+ await db
722
+ .insertInto("archive")
723
+ .columns(["user_id", "data", "archived_at"])
724
+ .expression(
725
+ db.selectFrom("user")
726
+ .select(["id", "metadata", sql`now()`.as("archived_at")])
727
+ .where("is_active", "=", false)
728
+ )
729
+ .execute();
730
+ ```
731
+
732
+ ### UPDATE Operations
733
+
734
+ ```typescript
735
+ // Simple update
736
+ await db
737
+ .updateTable("user")
738
+ .set({ is_active: false })
739
+ .where("id", "=", userId)
740
+ .execute();
741
+
742
+ // Update with expression
743
+ await db
744
+ .updateTable("product")
745
+ .set((eb) => ({
746
+ stock_quantity: eb("stock_quantity", "+", 10),
747
+ }))
748
+ .where("sku", "=", "ABC123")
749
+ .returning(["id", "stock_quantity"])
750
+ .executeTakeFirst();
751
+ ```
752
+
753
+ ## Migrations
754
+
755
+ ### Configuration (kysely.config.ts)
756
+
757
+ ```typescript
758
+ import { PostgresDialect } from "kysely";
759
+ import { defineConfig } from "kysely-ctl";
760
+ import pg from "pg";
761
+
762
+ export default defineConfig({
763
+ dialect: new PostgresDialect({
764
+ pool: new pg.Pool({
765
+ connectionString: process.env.DATABASE_URL,
766
+ }),
767
+ }),
768
+ migrations: {
769
+ migrationFolder: "src/db/migrations",
770
+ },
771
+ seeds: {
772
+ seedFolder: "src/db/seeds",
773
+ },
774
+ });
775
+ ```
776
+
777
+ ### Migration Commands
778
+
779
+ ```bash
780
+ npx kysely migrate:make migration-name # Create migration
781
+ npx kysely migrate:latest # Run all pending migrations
782
+ npx kysely migrate:down # Rollback last migration
783
+ npx kysely seed make seed-name # Create seed
784
+ npx kysely seed run # Run all seeds
785
+ ```
786
+
787
+ ### Migration File Structure
788
+
789
+ ```typescript
790
+ import type { Kysely } from "kysely";
791
+ import { sql } from "kysely";
792
+
793
+ // Always use Kysely<any> - migrations should be frozen in time
794
+ export async function up(db: Kysely<any>): Promise<void> {
795
+ await db.schema
796
+ .createTable("user")
797
+ .addColumn("id", "bigint", (col) => col.primaryKey().generatedAlwaysAsIdentity())
798
+ .addColumn("email", "text", (col) => col.notNull().unique())
799
+ .addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`))
800
+ .execute();
801
+
802
+ // IMPORTANT: Always index foreign key columns!
803
+ await db.schema.createIndex("idx_order_user_id").on("order").column("user_id").execute();
804
+ }
805
+
806
+ export async function down(db: Kysely<any>): Promise<void> {
807
+ await db.schema.dropTable("user").execute();
808
+ }
809
+ ```
810
+
811
+ ### Recommended Column Types
812
+
813
+ ```typescript
814
+ // Primary keys: Use identity columns (SQL standard, prevents accidental ID conflicts)
815
+ .addColumn("id", "bigint", (col) => col.primaryKey().generatedAlwaysAsIdentity())
816
+ // NOT serial/bigserial - those allow manual ID inserts that can cause conflicts
817
+
818
+ // Timestamps: Always use timestamptz (stores UTC, converts to client timezone)
819
+ .addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`))
820
+ // NOT timestamp - loses timezone information
821
+
822
+ // Money: Use numeric with precision (exact decimal, no floating point errors)
823
+ .addColumn("price", "numeric(10, 2)", (col) => col.notNull())
824
+ // NOT float/real/double precision - those have rounding errors
825
+
826
+ // Strings: Use text (no length limit, same performance as varchar)
827
+ .addColumn("name", "text", (col) => col.notNull())
828
+ // varchar(n) only if you need a hard length constraint
829
+
830
+ // JSON: Use jsonb (binary, indexable, faster queries)
831
+ .addColumn("metadata", "jsonb")
832
+ // NOT json - stored as text, no indexing, slower
833
+
834
+ // Foreign keys: Create indexes manually (PostgreSQL doesn't auto-index FKs)
835
+ await db.schema.createIndex("idx_order_user_id").on("order").column("user_id").execute();
836
+ ```
837
+
838
+ ### Data Type Gotchas
839
+
840
+ ```typescript
841
+ // CORRECT - Space after comma in numeric types
842
+ .addColumn("price", "numeric(10, 2)")
843
+
844
+ // WRONG - Will fail with "invalid column data type"
845
+ .addColumn("price", "numeric(10,2)")
846
+
847
+ // For complex types, use sql template
848
+ .addColumn("price", sql`numeric(10, 2)`)
849
+ ```
850
+
851
+ ## Type Generation
852
+
853
+ Use `kysely-codegen` to generate types from your database:
854
+
855
+ ```bash
856
+ npx kysely-codegen --url "postgresql://..." --out-file src/db/db.d.ts
857
+ ```
858
+
859
+ Generated types use:
860
+ - `Generated<T>` for auto-increment columns (optional on insert)
861
+ - `ColumnType<Select, Insert, Update>` for different operation types
862
+ - `Timestamp` for timestamptz columns
863
+
864
+ ## Common Pitfalls to Avoid
865
+
866
+ ### 1. Don't Resort to `sql`` When Kysely Has a Method
867
+
868
+ ```typescript
869
+ // WRONG
870
+ .select(sql`count(*)`.as("count"))
871
+
872
+ // RIGHT
873
+ .select((eb) => eb.fn.countAll().as("count"))
874
+
875
+ // WRONG - raw SQL for FILTER (WHERE ...) on aggregates
876
+ .having(sql<number>`count(*) filter (where status != 'signed')`, "=", 0)
877
+
878
+ // RIGHT - .filterWhere() works on all aggregate function builders
879
+ .having(
880
+ (eb) => eb.fn.countAll().filterWhere("status", "!=", "signed"),
881
+ "=",
882
+ 0
883
+ )
884
+ ```
885
+
886
+ ### 2. Don't Forget .execute()
887
+
888
+ Queries are lazy - they won't run without calling an execute method:
889
+
890
+ ```typescript
891
+ // This does nothing!
892
+ db.selectFrom("user").selectAll();
893
+
894
+ // This runs the query
895
+ await db.selectFrom("user").selectAll().execute();
896
+ ```
897
+
898
+ ### 3. Use whereRef for Column-to-Column Comparisons
899
+
900
+ ```typescript
901
+ // WRONG - Compares to string literal "other.column"
902
+ .where("table.column", "=", "other.column")
903
+
904
+ // RIGHT - Compares to actual column value
905
+ .whereRef("table.column", "=", "other.column")
906
+ ```
907
+
908
+ ### 4. Type Your Function Returns
909
+
910
+ ```typescript
911
+ // Better type inference
912
+ eb.fn<string>("concat", [...])
913
+ eb.fn<number>("length", [...])
914
+ ```
915
+
916
+ ### 5. PostgreSQL Does NOT Auto-Index Foreign Keys
917
+
918
+ Always create indexes on foreign key columns:
919
+
920
+ ```typescript
921
+ await db.schema.createIndex("idx_order_user_id").on("order").column("user_id").execute();
922
+ ```
923
+
924
+ ### 6. Always Type `sql` Template Literals
925
+
926
+ When using `sql` template literals, the inferred type is `unknown` since Kysely can't know what the SQL expression resolves to. Always provide an explicit type:
927
+
928
+ ```typescript
929
+ // WRONG - Returns unknown type
930
+ eb.fn.coalesce("some_json_col", sql`'{}'::jsonb`)
931
+
932
+ // RIGHT - Explicit type annotation
933
+ eb.fn.coalesce("some_json_col", sql<Record<string, unknown>>`'{}'::jsonb`)
934
+
935
+ // For complex types (e.g., JSON column from a CTE), use typeof with eb.ref
936
+ // This ensures the fallback type matches the column type exactly
937
+ eb.fn
938
+ .coalesce(
939
+ eb.ref("jobs_agg.jobs"),
940
+ sql<typeof eb.ref<"jobs_agg.jobs">>`'[]'::json`
941
+ )
942
+ .as("jobs")
943
+ ```
944
+
945
+ **Key rule**: Every `sql` template literal should have a type parameter: `sql<TYPE>`. This ensures proper type inference throughout your query chain.
946
+
947
+ ### 7. DATE Columns Cause Timezone Issues
948
+
949
+ By default, the `pg` driver converts DATE columns to JavaScript `Date` objects. This causes timezone problems:
950
+
951
+ ```
952
+ Database: 2025-01-01 (just a date, no time)
953
+ JS Date: 2025-01-01T00:00:00.000Z (interpreted as UTC midnight)
954
+ User in NYC sees: Dec 31, 2024 (5 hours behind UTC)
955
+ ```
956
+
957
+ **Solution: Parse DATE as string and let the frontend handle formatting**
958
+
959
+ Step 1: Configure `pg` to return DATE as string:
960
+
961
+ ```typescript
962
+ import pg from "pg";
963
+
964
+ // Tell pg to return DATE columns as strings instead of Date objects
965
+ const DATE_OID = 1082;
966
+ pg.types.setTypeParser(DATE_OID, (val: string) => val);
967
+ ```
968
+
969
+ Step 2: Update `kysely-codegen` to generate matching types:
970
+
971
+ ```bash
972
+ npx kysely-codegen \
973
+ --url="$DATABASE_URL" \
974
+ --out-file=server/db/db.d.ts \
975
+ --dialect=postgres \
976
+ --date-parser=string
977
+ ```
978
+
979
+ Now DATE columns return strings like `"2025-01-01"` and the frontend can parse/format respecting the user's timezone.
980
+
981
+ **Note**: This applies to DATE columns only. TIMESTAMPTZ columns already handle timezones correctly by storing UTC and converting on read.
982
+
983
+ ## PostgreSQL Helpers Summary
984
+
985
+ All helpers from `kysely/helpers/postgres`:
986
+
987
+ ```typescript
988
+ import {
989
+ jsonArrayFrom, // One-to-many relations (subquery → array)
990
+ jsonObjectFrom, // Many-to-one relations (subquery → object | null)
991
+ jsonBuildObject, // Build JSON object from expressions
992
+ mergeAction, // Get action performed in MERGE query (PostgreSQL 15+)
993
+ } from "kysely/helpers/postgres";
994
+ ```
995
+
996
+ **Note**: `jsonAgg` is NOT imported - use `eb.fn.jsonAgg()` instead.
997
+
998
+ ### mergeAction (PostgreSQL 15+)
999
+
1000
+ For MERGE queries, get which action was performed:
1001
+
1002
+ ```typescript
1003
+ import { mergeAction } from "kysely/helpers/postgres";
1004
+
1005
+ const result = await db
1006
+ .mergeInto("person")
1007
+ .using("person_updates", "person.id", "person_updates.id")
1008
+ .whenMatched()
1009
+ .thenUpdateSet({ name: eb.ref("person_updates.name") })
1010
+ .whenNotMatched()
1011
+ .thenInsertValues({ id: eb.ref("person_updates.id"), name: eb.ref("person_updates.name") })
1012
+ .returning([mergeAction().as("action"), "id"])
1013
+ .execute();
1014
+
1015
+ // result[0].action is 'INSERT' | 'UPDATE' | 'DELETE'
1016
+ ```
1017
+
1018
+ ## Extending Kysely
1019
+
1020
+ ### Custom Helper Functions
1021
+
1022
+ Most extensions use the `sql` template tag with `RawBuilder<T>`:
1023
+
1024
+ ```typescript
1025
+ import { sql, RawBuilder } from "kysely";
1026
+
1027
+ // Create a typed helper function
1028
+ function json<T>(value: T): RawBuilder<T> {
1029
+ return sql`CAST(${JSON.stringify(value)} AS JSONB)`;
1030
+ }
1031
+
1032
+ // Use in queries
1033
+ .select((eb) => [
1034
+ json({ name: "value" }).as("data"),
1035
+ ])
1036
+ ```
1037
+
1038
+ ### Custom Expression Classes
1039
+
1040
+ For reusable expressions, implement the `Expression<T>` interface:
1041
+
1042
+ ```typescript
1043
+ import { Expression, OperationNode, sql } from "kysely";
1044
+
1045
+ class JsonValue<T> implements Expression<T> {
1046
+ readonly #value: T;
1047
+
1048
+ constructor(value: T) {
1049
+ this.#value = value;
1050
+ }
1051
+
1052
+ get expressionType(): T | undefined {
1053
+ return undefined;
1054
+ }
1055
+
1056
+ toOperationNode(): OperationNode {
1057
+ return sql`CAST(${JSON.stringify(this.#value)} AS JSONB)`.toOperationNode();
1058
+ }
1059
+ }
1060
+ ```
1061
+
1062
+ **Note**: Module augmentation and inheritance-based extension are not recommended.
1063
+
1064
+ ## Handling "Excessively Deep Types" Error
1065
+
1066
+ ### The Problem
1067
+
1068
+ Complex queries with many CTEs can overwhelm TypeScript's type instantiation limits:
1069
+
1070
+ ```
1071
+ Type instantiation is excessively deep and possibly infinite
1072
+ ```
1073
+
1074
+ This commonly occurs with 12+ `with` clauses, as Kysely's nested helper types accumulate.
1075
+
1076
+ ### The Solution: `$assertType`
1077
+
1078
+ Use `$assertType` to simplify the type chain at intermediate points:
1079
+
1080
+ ```typescript
1081
+ const result = await db
1082
+ .with("cte1", (qb) =>
1083
+ qb.selectFrom("user")
1084
+ .select(["id", "email"])
1085
+ .$assertType<{ id: number; email: string }>() // Simplify type here
1086
+ )
1087
+ .with("cte2", (qb) =>
1088
+ qb.selectFrom("cte1")
1089
+ .select("email")
1090
+ .$assertType<{ email: string }>()
1091
+ )
1092
+ // ... more CTEs
1093
+ .selectFrom("cteN")
1094
+ .selectAll()
1095
+ .execute();
1096
+ ```
1097
+
1098
+ **Key points**:
1099
+ - The asserted type must structurally match the actual type (full type safety preserved)
1100
+ - Apply to several intermediate `with` clauses in large queries
1101
+ - TypeScript cannot automatically simplify these types - explicit assertion is required