@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.
- package/README.md +137 -0
- package/package.json +26 -0
- package/plugins/doctl/.claude-plugin/plugin.json +8 -0
- package/plugins/doctl/skills/doctl/SKILL.md +93 -0
- package/plugins/kysely-postgres/.claude-plugin/plugin.json +8 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +1101 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/aggregations.ts +167 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/ctes.ts +165 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/expressions.ts +272 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/joins.ts +206 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/json-arrays.ts +398 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/mutations.ts +199 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/orderby-pagination.ts +117 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/relations.ts +176 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/select-where.ts +146 -0
- package/plugins/linear/.claude-plugin/plugin.json +8 -0
- package/plugins/linear/skills/linear/SKILL.md +1040 -0
- package/plugins/linear/skills/linear/bin/linear.mjs +1228 -0
- package/plugins/linear/skills/linear/tech-stack.md +273 -0
- package/plugins/nitro-testing/.claude-plugin/plugin.json +8 -0
- package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +497 -0
- package/plugins/nitro-testing/skills/nitro-testing/async-testing.md +270 -0
- package/plugins/nitro-testing/skills/nitro-testing/ci-setup.md +226 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/global-setup.ts +90 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/handler.test.ts +167 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/setup.ts +29 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/test-utils-index.ts +297 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/vitest.config.ts +42 -0
- package/plugins/nitro-testing/skills/nitro-testing/factories.md +278 -0
- package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +512 -0
- package/plugins/nitro-testing/skills/nitro-testing/test-utils.md +262 -0
- package/plugins/nitro-testing/skills/nitro-testing/transaction-rollback.md +183 -0
- package/plugins/nitro-testing/skills/nitro-testing/vitest-config.md +236 -0
- package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +8 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +260 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +228 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +174 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/deep-linking.md +190 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-middleware.ts +32 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-utils.ts +51 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/deep-link-page.vue +61 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/service-util.ts +63 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/sse-endpoint.ts +59 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/validation-endpoint.ts +38 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +178 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/nitro-tasks.md +243 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +162 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-services.md +238 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/sse.md +221 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +166 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/validation.md +131 -0
- package/scripts/link-skills.mjs +252 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOIN Patterns
|
|
3
|
+
* Simple joins, multi-table joins, and complex callback joins
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
import { sql } from "kysely";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// BASIC JOINS
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
// Inner join - simple format
|
|
13
|
+
const ordersWithUsers = await db
|
|
14
|
+
.selectFrom("order")
|
|
15
|
+
.innerJoin("user", "user.id", "order.user_id")
|
|
16
|
+
.select([
|
|
17
|
+
"order.id as orderId",
|
|
18
|
+
"order.status",
|
|
19
|
+
"user.email",
|
|
20
|
+
"user.first_name",
|
|
21
|
+
])
|
|
22
|
+
.execute();
|
|
23
|
+
|
|
24
|
+
// Left join - includes rows without matches
|
|
25
|
+
const productsWithCategories = await db
|
|
26
|
+
.selectFrom("product")
|
|
27
|
+
.leftJoin("category", "category.id", "product.category_id")
|
|
28
|
+
.select([
|
|
29
|
+
"product.name as productName",
|
|
30
|
+
"category.name as categoryName", // null if no category
|
|
31
|
+
])
|
|
32
|
+
.execute();
|
|
33
|
+
|
|
34
|
+
// Multiple joins - chain them
|
|
35
|
+
const orderDetails = await db
|
|
36
|
+
.selectFrom("order_item")
|
|
37
|
+
.innerJoin("order", "order.id", "order_item.order_id")
|
|
38
|
+
.innerJoin("product", "product.id", "order_item.product_id")
|
|
39
|
+
.innerJoin("user", "user.id", "order.user_id")
|
|
40
|
+
.select([
|
|
41
|
+
"user.email",
|
|
42
|
+
"product.name as productName",
|
|
43
|
+
"order_item.quantity",
|
|
44
|
+
"order_item.unit_price",
|
|
45
|
+
])
|
|
46
|
+
.execute();
|
|
47
|
+
|
|
48
|
+
// Self-join with aliases
|
|
49
|
+
const categoriesWithParent = await db
|
|
50
|
+
.selectFrom("category as c")
|
|
51
|
+
.leftJoin("category as parent", "parent.id", "c.parent_id")
|
|
52
|
+
.select([
|
|
53
|
+
"c.name as categoryName",
|
|
54
|
+
"parent.name as parentCategoryName",
|
|
55
|
+
])
|
|
56
|
+
.execute();
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// COMPLEX JOINS (Callback Format)
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
// When to use callback format:
|
|
63
|
+
// 1. Multiple join conditions (composite keys)
|
|
64
|
+
// 2. Mixed column-to-column AND column-to-literal
|
|
65
|
+
// 3. OR conditions in joins
|
|
66
|
+
// 4. Subquery joins (derived tables)
|
|
67
|
+
|
|
68
|
+
// Multi-condition join: onRef + on
|
|
69
|
+
// onRef = column-to-column, on = column-to-literal
|
|
70
|
+
const activeProductItems = await db
|
|
71
|
+
.selectFrom("order_item as oi")
|
|
72
|
+
.innerJoin("product as p", (join) =>
|
|
73
|
+
join
|
|
74
|
+
.onRef("p.id", "=", "oi.product_id")
|
|
75
|
+
.on("p.is_active", "=", true) // Filter in join, not WHERE
|
|
76
|
+
)
|
|
77
|
+
.select([
|
|
78
|
+
"oi.id as orderItemId",
|
|
79
|
+
"p.name as productName",
|
|
80
|
+
"oi.quantity",
|
|
81
|
+
])
|
|
82
|
+
.execute();
|
|
83
|
+
|
|
84
|
+
// Join with OR conditions
|
|
85
|
+
const usersWithCompletedOrShipped = await db
|
|
86
|
+
.selectFrom("user as u")
|
|
87
|
+
.leftJoin("order as o", (join) =>
|
|
88
|
+
join
|
|
89
|
+
.onRef("o.user_id", "=", "u.id")
|
|
90
|
+
.on((eb) =>
|
|
91
|
+
eb.or([
|
|
92
|
+
eb("o.status", "=", "completed"),
|
|
93
|
+
eb("o.status", "=", "shipped"),
|
|
94
|
+
])
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
.select([
|
|
98
|
+
"u.email",
|
|
99
|
+
"o.id as orderId",
|
|
100
|
+
"o.status",
|
|
101
|
+
])
|
|
102
|
+
.execute();
|
|
103
|
+
|
|
104
|
+
// ============================================
|
|
105
|
+
// SUBQUERY JOINS (Derived Tables)
|
|
106
|
+
// ============================================
|
|
107
|
+
|
|
108
|
+
// Two-callback format: first builds subquery, second defines join
|
|
109
|
+
const usersWithOrderStats = await db
|
|
110
|
+
.selectFrom("user as u")
|
|
111
|
+
.leftJoin(
|
|
112
|
+
// First callback: build the subquery
|
|
113
|
+
(eb) =>
|
|
114
|
+
eb
|
|
115
|
+
.selectFrom("order")
|
|
116
|
+
.select((eb) => [
|
|
117
|
+
"user_id",
|
|
118
|
+
eb.fn.count("id").as("order_count"),
|
|
119
|
+
eb.fn.sum("total_amount").as("total_spent"),
|
|
120
|
+
])
|
|
121
|
+
.groupBy("user_id")
|
|
122
|
+
.as("order_stats"), // MUST have alias!
|
|
123
|
+
// Second callback: define join condition
|
|
124
|
+
(join) => join.onRef("order_stats.user_id", "=", "u.id")
|
|
125
|
+
)
|
|
126
|
+
.select([
|
|
127
|
+
"u.email",
|
|
128
|
+
"u.first_name",
|
|
129
|
+
"order_stats.order_count",
|
|
130
|
+
"order_stats.total_spent",
|
|
131
|
+
])
|
|
132
|
+
.execute();
|
|
133
|
+
|
|
134
|
+
// Subquery join with MAX aggregation
|
|
135
|
+
const productsWithLatestReview = await db
|
|
136
|
+
.selectFrom("product as p")
|
|
137
|
+
.leftJoin(
|
|
138
|
+
(eb) =>
|
|
139
|
+
eb
|
|
140
|
+
.selectFrom("review")
|
|
141
|
+
.select((eb) => [
|
|
142
|
+
"product_id",
|
|
143
|
+
eb.fn.max("created_at").as("latest_review_at"),
|
|
144
|
+
eb.fn.count("id").as("review_count"),
|
|
145
|
+
])
|
|
146
|
+
.groupBy("product_id")
|
|
147
|
+
.as("review_stats"),
|
|
148
|
+
(join) => join.onRef("review_stats.product_id", "=", "p.id")
|
|
149
|
+
)
|
|
150
|
+
.select([
|
|
151
|
+
"p.name",
|
|
152
|
+
"p.price",
|
|
153
|
+
"review_stats.latest_review_at",
|
|
154
|
+
"review_stats.review_count",
|
|
155
|
+
])
|
|
156
|
+
.orderBy("review_stats.review_count", (ob) => ob.desc().nullsLast())
|
|
157
|
+
.execute();
|
|
158
|
+
|
|
159
|
+
// ============================================
|
|
160
|
+
// CROSS JOIN
|
|
161
|
+
// ============================================
|
|
162
|
+
|
|
163
|
+
// Cross join using always-true condition
|
|
164
|
+
// Use case: Join CTE aggregation to main query
|
|
165
|
+
const usersWithSummary = await db
|
|
166
|
+
.with("summary", (db) =>
|
|
167
|
+
db
|
|
168
|
+
.selectFrom("order")
|
|
169
|
+
.select((eb) => [
|
|
170
|
+
eb.fn.count("id").as("total_orders"),
|
|
171
|
+
eb.fn.sum("total_amount").as("total_revenue"),
|
|
172
|
+
])
|
|
173
|
+
)
|
|
174
|
+
.selectFrom("user as u")
|
|
175
|
+
.leftJoin("summary", (join) =>
|
|
176
|
+
join.on(sql`true`, "=", sql`true`)
|
|
177
|
+
)
|
|
178
|
+
.select([
|
|
179
|
+
"u.email",
|
|
180
|
+
"summary.total_orders",
|
|
181
|
+
"summary.total_revenue",
|
|
182
|
+
])
|
|
183
|
+
.where("u.role", "=", "admin")
|
|
184
|
+
.execute();
|
|
185
|
+
|
|
186
|
+
// ============================================
|
|
187
|
+
// KEY PATTERNS SUMMARY
|
|
188
|
+
// ============================================
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
1. Simple joins: .innerJoin("table", "a.col", "b.col")
|
|
192
|
+
- Use when joining on single column equality
|
|
193
|
+
|
|
194
|
+
2. Callback joins: .innerJoin("table", (join) => join.onRef(...).on(...))
|
|
195
|
+
- onRef(col1, op, col2): column-to-column
|
|
196
|
+
- on(col, op, value): column-to-literal
|
|
197
|
+
- on((eb) => eb.or([...])): OR conditions
|
|
198
|
+
|
|
199
|
+
3. Subquery joins: Two callbacks
|
|
200
|
+
- First: (eb) => eb.selectFrom(...).as("alias")
|
|
201
|
+
- Second: (join) => join.onRef(...)
|
|
202
|
+
- ALWAYS include .as("alias") on subquery!
|
|
203
|
+
|
|
204
|
+
4. Cross joins: join.on(sql`true`, "=", sql`true`)
|
|
205
|
+
- For joining unrelated data (like CTE summaries)
|
|
206
|
+
*/
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON/JSONB and Array Patterns
|
|
3
|
+
* PostgreSQL-specific JSON functions and array handling
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
import { sql } from "kysely";
|
|
7
|
+
import { jsonBuildObject } from "kysely/helpers/postgres";
|
|
8
|
+
|
|
9
|
+
// ============================================
|
|
10
|
+
// JSONB INSERT/UPDATE - NO JSON.stringify!
|
|
11
|
+
// ============================================
|
|
12
|
+
|
|
13
|
+
// The pg driver handles JSONB serialization automatically
|
|
14
|
+
// Just pass JavaScript objects directly!
|
|
15
|
+
|
|
16
|
+
// Insert with JSONB
|
|
17
|
+
const newUser = await db
|
|
18
|
+
.insertInto("user")
|
|
19
|
+
.values({
|
|
20
|
+
email: "test@example.com",
|
|
21
|
+
first_name: "Test",
|
|
22
|
+
last_name: "User",
|
|
23
|
+
metadata: { preferences: { theme: "dark" }, notifications: true } as any,
|
|
24
|
+
})
|
|
25
|
+
.returning(["id", "email", "metadata"])
|
|
26
|
+
.executeTakeFirst();
|
|
27
|
+
|
|
28
|
+
// Update JSONB
|
|
29
|
+
const updatedUser = await db
|
|
30
|
+
.updateTable("user")
|
|
31
|
+
.set({
|
|
32
|
+
metadata: { preferences: { theme: "light" }, notifications: false } as any,
|
|
33
|
+
})
|
|
34
|
+
.where("email", "=", "test@example.com")
|
|
35
|
+
.returning(["id", "metadata"])
|
|
36
|
+
.executeTakeFirst();
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// JSONB READ - NO JSON.parse!
|
|
40
|
+
// ============================================
|
|
41
|
+
|
|
42
|
+
// Returns parsed objects automatically
|
|
43
|
+
const user = await db
|
|
44
|
+
.selectFrom("user")
|
|
45
|
+
.select(["id", "email", "metadata"])
|
|
46
|
+
.where("email", "=", "test@example.com")
|
|
47
|
+
.executeTakeFirst();
|
|
48
|
+
|
|
49
|
+
// Access directly - already an object!
|
|
50
|
+
console.log(user?.metadata?.preferences?.theme); // "light"
|
|
51
|
+
console.log(typeof user?.metadata); // "object"
|
|
52
|
+
|
|
53
|
+
// ============================================
|
|
54
|
+
// ARRAY COLUMNS - NO JSON.stringify!
|
|
55
|
+
// ============================================
|
|
56
|
+
|
|
57
|
+
// Insert with text[] array - pass array directly
|
|
58
|
+
const product = await db
|
|
59
|
+
.insertInto("product")
|
|
60
|
+
.values({
|
|
61
|
+
name: "Test Product",
|
|
62
|
+
sku: "TEST-001",
|
|
63
|
+
price: "19.99",
|
|
64
|
+
tags: ["electronics", "sale", "featured"], // Direct array!
|
|
65
|
+
})
|
|
66
|
+
.returning(["id", "name", "tags"])
|
|
67
|
+
.executeTakeFirst();
|
|
68
|
+
|
|
69
|
+
// Read array - returns native JavaScript array
|
|
70
|
+
const productWithTags = await db
|
|
71
|
+
.selectFrom("product")
|
|
72
|
+
.select(["name", "tags"])
|
|
73
|
+
.where("sku", "=", "TEST-001")
|
|
74
|
+
.executeTakeFirst();
|
|
75
|
+
|
|
76
|
+
console.log(productWithTags?.tags); // ["electronics", "sale", "featured"]
|
|
77
|
+
console.log(Array.isArray(productWithTags?.tags)); // true
|
|
78
|
+
|
|
79
|
+
// Update array
|
|
80
|
+
await db
|
|
81
|
+
.updateTable("product")
|
|
82
|
+
.set({ tags: ["updated", "tags"] })
|
|
83
|
+
.where("sku", "=", "TEST-001")
|
|
84
|
+
.execute();
|
|
85
|
+
|
|
86
|
+
// ============================================
|
|
87
|
+
// ARRAY QUERIES
|
|
88
|
+
// ============================================
|
|
89
|
+
|
|
90
|
+
// Array contains all (@>) - operator works natively!
|
|
91
|
+
const hasAllTags = await db
|
|
92
|
+
.selectFrom("product")
|
|
93
|
+
.select(["name", "tags"])
|
|
94
|
+
.where("tags", "@>", sql`ARRAY['electronics', 'premium']::text[]`)
|
|
95
|
+
.execute();
|
|
96
|
+
|
|
97
|
+
// Array overlap (&&) - operator works natively!
|
|
98
|
+
const premiumOrBasic = await db
|
|
99
|
+
.selectFrom("product")
|
|
100
|
+
.select(["name", "tags"])
|
|
101
|
+
.where("tags", "&&", sql`ARRAY['premium', 'basic']::text[]`)
|
|
102
|
+
.execute();
|
|
103
|
+
|
|
104
|
+
// Array contains value (ANY) - type-safe with eb.fn
|
|
105
|
+
// eb.ref("tags") validates column exists at compile time
|
|
106
|
+
const searchTag = "phone";
|
|
107
|
+
const phonesProducts = await db
|
|
108
|
+
.selectFrom("product")
|
|
109
|
+
.select(["name", "tags"])
|
|
110
|
+
.where((eb) => eb(sql`${searchTag}`, "=", eb.fn("any", [eb.ref("tags")])))
|
|
111
|
+
.execute();
|
|
112
|
+
// Using eb.ref("invalid_column") would be a TypeScript error!
|
|
113
|
+
|
|
114
|
+
// ============================================
|
|
115
|
+
// JSONB QUERIES - Operators that work natively
|
|
116
|
+
// ============================================
|
|
117
|
+
|
|
118
|
+
// Key exists (?) - works as native operator!
|
|
119
|
+
const hasThemeKey = await db
|
|
120
|
+
.selectFrom("user")
|
|
121
|
+
.selectAll()
|
|
122
|
+
.where("metadata", "?", "theme")
|
|
123
|
+
.execute();
|
|
124
|
+
|
|
125
|
+
// Any key exists (?|) - works as native operator!
|
|
126
|
+
const hasAnyKey = await db
|
|
127
|
+
.selectFrom("user")
|
|
128
|
+
.selectAll()
|
|
129
|
+
.where("metadata", "?|", sql`array['theme', 'language']`)
|
|
130
|
+
.execute();
|
|
131
|
+
|
|
132
|
+
// All keys exist (?&) - works as native operator!
|
|
133
|
+
const hasAllKeys = await db
|
|
134
|
+
.selectFrom("user")
|
|
135
|
+
.selectAll()
|
|
136
|
+
.where("metadata", "?&", sql`array['theme', 'notifications']`)
|
|
137
|
+
.execute();
|
|
138
|
+
|
|
139
|
+
// JSONB contains (@>) - works as native operator!
|
|
140
|
+
const usersWithPrefs = await db
|
|
141
|
+
.selectFrom("user")
|
|
142
|
+
.selectAll()
|
|
143
|
+
.where("metadata", "@>", sql`'{"notifications": true}'::jsonb`)
|
|
144
|
+
.execute();
|
|
145
|
+
|
|
146
|
+
// JSONB contained by (<@) - works as native operator!
|
|
147
|
+
const simpleMetadata = await db
|
|
148
|
+
.selectFrom("user")
|
|
149
|
+
.selectAll()
|
|
150
|
+
.where("metadata", "<@", sql`'{"theme": "dark", "notifications": true}'::jsonb`)
|
|
151
|
+
.execute();
|
|
152
|
+
|
|
153
|
+
// ============================================
|
|
154
|
+
// JSONB QUERIES - Extraction
|
|
155
|
+
// ============================================
|
|
156
|
+
|
|
157
|
+
// Single-level extraction: -> and ->> work with eb() - type-safe!
|
|
158
|
+
const extracted = await db
|
|
159
|
+
.selectFrom("user")
|
|
160
|
+
.select((eb) => [
|
|
161
|
+
"id",
|
|
162
|
+
eb("metadata", "->", "preferences").as("preferences"), // Returns JSONB
|
|
163
|
+
eb("metadata", "->>", "theme").as("theme"), // Returns text
|
|
164
|
+
])
|
|
165
|
+
.execute();
|
|
166
|
+
|
|
167
|
+
// Nested path extraction: #> and #>> need sql``
|
|
168
|
+
const nestedJson = await db
|
|
169
|
+
.selectFrom("user")
|
|
170
|
+
.select([
|
|
171
|
+
"id",
|
|
172
|
+
sql`metadata#>'{preferences,theme}'`.as("theme"), // Returns JSONB
|
|
173
|
+
])
|
|
174
|
+
.execute();
|
|
175
|
+
|
|
176
|
+
const nestedText = await db
|
|
177
|
+
.selectFrom("user")
|
|
178
|
+
.select([
|
|
179
|
+
"id",
|
|
180
|
+
sql<string>`metadata#>>'{preferences,theme}'`.as("theme"), // Returns text
|
|
181
|
+
])
|
|
182
|
+
.execute();
|
|
183
|
+
|
|
184
|
+
// Filter by JSON field value - type-safe!
|
|
185
|
+
const darkThemeUsers = await db
|
|
186
|
+
.selectFrom("user")
|
|
187
|
+
.selectAll()
|
|
188
|
+
.where((eb) => eb(eb("metadata", "->>", "theme"), "=", "dark"))
|
|
189
|
+
.execute();
|
|
190
|
+
// eb("metadata", ...) validates column - eb("invalid", ...) would be TS error
|
|
191
|
+
|
|
192
|
+
// Filter by nested JSON value (path syntax still needs sql``)
|
|
193
|
+
const specificUsers = await db
|
|
194
|
+
.selectFrom("user")
|
|
195
|
+
.selectAll()
|
|
196
|
+
.where(sql`metadata#>>'{preferences,theme}'`, "=", "dark")
|
|
197
|
+
.execute();
|
|
198
|
+
|
|
199
|
+
// ============================================
|
|
200
|
+
// JSONPath (PostgreSQL 12+)
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
// JSONPath match (@@) - works as native operator!
|
|
204
|
+
const matchingUsers = await db
|
|
205
|
+
.selectFrom("user")
|
|
206
|
+
.selectAll()
|
|
207
|
+
.where("metadata", "@@", sql`'$.preferences.theme == "dark"'`)
|
|
208
|
+
.execute();
|
|
209
|
+
|
|
210
|
+
// JSONPath exists (@?) - NOT in Kysely's allowlist, use function instead
|
|
211
|
+
// Use jsonb_path_exists() for type-safe column validation
|
|
212
|
+
const usersWithTheme = await db
|
|
213
|
+
.selectFrom("user")
|
|
214
|
+
.selectAll()
|
|
215
|
+
.where((eb) =>
|
|
216
|
+
eb.fn("jsonb_path_exists", [eb.ref("metadata"), sql`'$.preferences.theme'`])
|
|
217
|
+
)
|
|
218
|
+
.execute();
|
|
219
|
+
// eb.ref("metadata") validates column exists - eb.ref("invalid") would be TS error
|
|
220
|
+
|
|
221
|
+
// Extract value with JSONPath - type-safe with eb.fn
|
|
222
|
+
const themes = await db
|
|
223
|
+
.selectFrom("user")
|
|
224
|
+
.select((eb) => [
|
|
225
|
+
"id",
|
|
226
|
+
eb
|
|
227
|
+
.fn("jsonb_path_query_first", [
|
|
228
|
+
eb.ref("metadata"),
|
|
229
|
+
sql`'$.preferences.theme'`,
|
|
230
|
+
])
|
|
231
|
+
.as("theme"),
|
|
232
|
+
])
|
|
233
|
+
.execute();
|
|
234
|
+
|
|
235
|
+
// JSONPath with variables
|
|
236
|
+
const searchValue = "dark";
|
|
237
|
+
const filtered = await db
|
|
238
|
+
.selectFrom("user")
|
|
239
|
+
.selectAll()
|
|
240
|
+
.where((eb) =>
|
|
241
|
+
eb.fn("jsonb_path_exists", [
|
|
242
|
+
eb.ref("metadata"),
|
|
243
|
+
sql`'$.preferences.theme ? (@ == $val)'`,
|
|
244
|
+
sql`jsonb_build_object('val', ${searchValue}::text)`,
|
|
245
|
+
])
|
|
246
|
+
)
|
|
247
|
+
.execute();
|
|
248
|
+
|
|
249
|
+
// ============================================
|
|
250
|
+
// jsonBuildObject
|
|
251
|
+
// ============================================
|
|
252
|
+
|
|
253
|
+
// Build JSON objects in SELECT
|
|
254
|
+
const usersWithInfo = await db
|
|
255
|
+
.selectFrom("user")
|
|
256
|
+
.select((eb) => [
|
|
257
|
+
"id",
|
|
258
|
+
jsonBuildObject({
|
|
259
|
+
fullName: eb.fn<string>("concat", [
|
|
260
|
+
eb.ref("first_name"),
|
|
261
|
+
eb.cast(eb.val(" "), "text"),
|
|
262
|
+
eb.ref("last_name"),
|
|
263
|
+
]),
|
|
264
|
+
email: eb.ref("email"),
|
|
265
|
+
role: eb.ref("role"),
|
|
266
|
+
}).as("userInfo"),
|
|
267
|
+
])
|
|
268
|
+
.execute();
|
|
269
|
+
|
|
270
|
+
// ============================================
|
|
271
|
+
// jsonAgg with CTEs
|
|
272
|
+
// ============================================
|
|
273
|
+
|
|
274
|
+
// Aggregate rows into JSON array
|
|
275
|
+
const usersWithOrders = await db
|
|
276
|
+
.with("user_orders", (db) =>
|
|
277
|
+
db
|
|
278
|
+
.selectFrom("order")
|
|
279
|
+
.innerJoin("user", "user.id", "order.user_id")
|
|
280
|
+
.select((eb) => [
|
|
281
|
+
"user.id as userId",
|
|
282
|
+
"user.email",
|
|
283
|
+
eb.fn
|
|
284
|
+
.jsonAgg(
|
|
285
|
+
jsonBuildObject({
|
|
286
|
+
orderId: eb.ref("order.id"),
|
|
287
|
+
status: eb.ref("order.status"),
|
|
288
|
+
total: eb.ref("order.total_amount"),
|
|
289
|
+
})
|
|
290
|
+
)
|
|
291
|
+
.as("orders"),
|
|
292
|
+
])
|
|
293
|
+
.groupBy(["user.id", "user.email"])
|
|
294
|
+
)
|
|
295
|
+
.selectFrom("user_orders")
|
|
296
|
+
.selectAll()
|
|
297
|
+
.execute();
|
|
298
|
+
|
|
299
|
+
// ============================================
|
|
300
|
+
// NESTED jsonAgg
|
|
301
|
+
// ============================================
|
|
302
|
+
|
|
303
|
+
// Multiple levels of nesting
|
|
304
|
+
const productsWithReviews = await db
|
|
305
|
+
.with("product_with_reviews", (db) =>
|
|
306
|
+
db
|
|
307
|
+
.selectFrom("product")
|
|
308
|
+
.leftJoin("review", "review.product_id", "product.id")
|
|
309
|
+
.leftJoin("user", "user.id", "review.user_id")
|
|
310
|
+
.select((eb) => [
|
|
311
|
+
"product.id as productId",
|
|
312
|
+
"product.name as productName",
|
|
313
|
+
eb.fn
|
|
314
|
+
.jsonAgg(
|
|
315
|
+
jsonBuildObject({
|
|
316
|
+
reviewId: eb.ref("review.id"),
|
|
317
|
+
rating: eb.ref("review.rating"),
|
|
318
|
+
title: eb.ref("review.title"),
|
|
319
|
+
// Nested object!
|
|
320
|
+
reviewer: jsonBuildObject({
|
|
321
|
+
name: eb.fn<string>("concat", [
|
|
322
|
+
eb.ref("user.first_name"),
|
|
323
|
+
eb.cast(eb.val(" "), "text"),
|
|
324
|
+
eb.ref("user.last_name"),
|
|
325
|
+
]),
|
|
326
|
+
email: eb.ref("user.email"),
|
|
327
|
+
}),
|
|
328
|
+
})
|
|
329
|
+
)
|
|
330
|
+
// .filterWhere() is available on ALL aggregate functions (count, sum, avg, etc.),
|
|
331
|
+
// not just jsonAgg. See aggregations.ts for general usage.
|
|
332
|
+
.filterWhere("review.id", "is not", null) // Filter nulls!
|
|
333
|
+
.as("reviews"),
|
|
334
|
+
])
|
|
335
|
+
.groupBy(["product.id", "product.name"])
|
|
336
|
+
)
|
|
337
|
+
.selectFrom("product_with_reviews")
|
|
338
|
+
.selectAll()
|
|
339
|
+
.where("reviews", "is not", null)
|
|
340
|
+
.execute();
|
|
341
|
+
|
|
342
|
+
// ============================================
|
|
343
|
+
// KEY PATTERNS SUMMARY
|
|
344
|
+
// ============================================
|
|
345
|
+
|
|
346
|
+
/*
|
|
347
|
+
1. JSONB columns:
|
|
348
|
+
- INSERT/UPDATE: Pass objects directly (no JSON.stringify)
|
|
349
|
+
- SELECT: Returns parsed objects (no JSON.parse)
|
|
350
|
+
- The pg driver handles serialization automatically
|
|
351
|
+
|
|
352
|
+
2. Array columns (text[], int[], etc.):
|
|
353
|
+
- INSERT/UPDATE: Pass arrays directly (no JSON.stringify)
|
|
354
|
+
- SELECT: Returns native JavaScript arrays
|
|
355
|
+
- The pg driver handles this automatically
|
|
356
|
+
|
|
357
|
+
3. Array queries - operators work natively:
|
|
358
|
+
- @> : .where("tags", "@>", sql`ARRAY[...]::text[]`)
|
|
359
|
+
- && : .where("tags", "&&", sql`ARRAY[...]::text[]`)
|
|
360
|
+
- ANY : .where((eb) => eb(sql`${val}`, "=", eb.fn("any", [eb.ref("tags")])))
|
|
361
|
+
eb.ref() provides type-safety - invalid columns are TS errors
|
|
362
|
+
|
|
363
|
+
4. JSONB queries - operators that work natively:
|
|
364
|
+
- ? : .where("col", "?", "key")
|
|
365
|
+
- ?| : .where("col", "?|", sql`array[...]`)
|
|
366
|
+
- ?& : .where("col", "?&", sql`array[...]`)
|
|
367
|
+
- @> : .where("col", "@>", sql`'{...}'::jsonb`)
|
|
368
|
+
- <@ : .where("col", "<@", sql`'{...}'::jsonb`)
|
|
369
|
+
|
|
370
|
+
Extraction operators:
|
|
371
|
+
- ->> : eb(eb("col", "->>", "key"), "=", val) (type-safe!)
|
|
372
|
+
- -> : eb("col", "->", "key") (returns JSONB)
|
|
373
|
+
- #> : sql`col#>'{a,b}'` (nested path, needs sql``)
|
|
374
|
+
- #>> : sql`col#>>'{a,b}'` (nested path, needs sql``)
|
|
375
|
+
|
|
376
|
+
5. JSONPath (PostgreSQL 12+):
|
|
377
|
+
- @@ : .where("col", "@@", sql`'$.path == "val"'`) (native operator!)
|
|
378
|
+
- @? : NOT in allowlist - use jsonb_path_exists() instead
|
|
379
|
+
- Functions (type-safe with eb.fn):
|
|
380
|
+
- jsonb_path_exists(col, path)
|
|
381
|
+
- jsonb_path_query_first(col, path)
|
|
382
|
+
- eb.ref() provides type-safety for column references
|
|
383
|
+
|
|
384
|
+
6. jsonBuildObject:
|
|
385
|
+
- Import from "kysely/helpers/postgres"
|
|
386
|
+
- Use eb.ref() for column references
|
|
387
|
+
- Can be nested for deep structures
|
|
388
|
+
|
|
389
|
+
7. jsonAgg:
|
|
390
|
+
- Use eb.fn.jsonAgg() (NOT imported from helpers!)
|
|
391
|
+
- Use .filterWhere() to exclude nulls
|
|
392
|
+
- Combine with jsonBuildObject for structured arrays
|
|
393
|
+
|
|
394
|
+
8. .filterWhere() (PostgreSQL FILTER (WHERE ...)):
|
|
395
|
+
- Available on ALL aggregate builders, not just jsonAgg
|
|
396
|
+
- Works on: count, countAll, sum, avg, min, max, jsonAgg
|
|
397
|
+
- See aggregations.ts for general examples
|
|
398
|
+
*/
|