@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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregation Patterns
|
|
3
|
+
* COUNT, SUM, AVG, GROUP BY, HAVING
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// BASIC AGGREGATIONS
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
// COUNT - total rows
|
|
12
|
+
const totalUsers = await db
|
|
13
|
+
.selectFrom("user")
|
|
14
|
+
.select((eb) => eb.fn.count("id").as("totalUsers"))
|
|
15
|
+
.executeTakeFirst();
|
|
16
|
+
// Result: { totalUsers: "5" } - Note: returned as string!
|
|
17
|
+
|
|
18
|
+
// COUNT with GROUP BY
|
|
19
|
+
const usersByRole = await db
|
|
20
|
+
.selectFrom("user")
|
|
21
|
+
.select((eb) => [
|
|
22
|
+
"role",
|
|
23
|
+
eb.fn.count("id").as("count"),
|
|
24
|
+
])
|
|
25
|
+
.groupBy("role")
|
|
26
|
+
.execute();
|
|
27
|
+
// Result: [{ role: "admin", count: "1" }, { role: "user", count: "3" }]
|
|
28
|
+
|
|
29
|
+
// SUM
|
|
30
|
+
const totalRevenue = await db
|
|
31
|
+
.selectFrom("order")
|
|
32
|
+
.where("status", "=", "completed")
|
|
33
|
+
.select((eb) => eb.fn.sum("total_amount").as("totalRevenue"))
|
|
34
|
+
.executeTakeFirst();
|
|
35
|
+
|
|
36
|
+
// AVG
|
|
37
|
+
const averageRating = await db
|
|
38
|
+
.selectFrom("review")
|
|
39
|
+
.select((eb) => eb.fn.avg("rating").as("averageRating"))
|
|
40
|
+
.executeTakeFirst();
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// MULTIPLE AGGREGATIONS
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
// Multiple aggregations with GROUP BY
|
|
47
|
+
const orderStats = await db
|
|
48
|
+
.selectFrom("order")
|
|
49
|
+
.select((eb) => [
|
|
50
|
+
"status",
|
|
51
|
+
eb.fn.count("id").as("orderCount"),
|
|
52
|
+
eb.fn.sum("total_amount").as("totalAmount"),
|
|
53
|
+
eb.fn.avg("total_amount").as("avgAmount"),
|
|
54
|
+
])
|
|
55
|
+
.groupBy("status")
|
|
56
|
+
.execute();
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// HAVING CLAUSE
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
// HAVING - filter aggregated results
|
|
63
|
+
// Use HAVING for conditions on aggregated values
|
|
64
|
+
// Use WHERE for conditions on individual rows
|
|
65
|
+
const popularProducts = await db
|
|
66
|
+
.selectFrom("review")
|
|
67
|
+
.innerJoin("product", "product.id", "review.product_id")
|
|
68
|
+
.select((eb) => [
|
|
69
|
+
"product.name",
|
|
70
|
+
eb.fn.count("review.id").as("reviewCount"),
|
|
71
|
+
eb.fn.avg("review.rating").as("avgRating"),
|
|
72
|
+
])
|
|
73
|
+
.groupBy("product.id")
|
|
74
|
+
.groupBy("product.name") // Group by all non-aggregated columns
|
|
75
|
+
.having((eb) => eb.fn.count("review.id"), ">", 1)
|
|
76
|
+
.execute();
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// FILTER (WHERE ...) ON AGGREGATES
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
// PostgreSQL's FILTER (WHERE ...) clause works on ALL aggregate function builders
|
|
83
|
+
// via .filterWhere(). This is NOT specific to jsonAgg — it works on count, sum, avg, etc.
|
|
84
|
+
|
|
85
|
+
// Basic usage: conditional counting/summing without CASE expressions
|
|
86
|
+
const filteredAggregates = await db
|
|
87
|
+
.selectFrom("user")
|
|
88
|
+
.select((eb) => [
|
|
89
|
+
eb.fn.count("id").filterWhere("status", "=", "active").as("active_count"),
|
|
90
|
+
eb.fn.countAll().filterWhere("role", "!=", "banned").as("non_banned"),
|
|
91
|
+
eb.fn.sum("balance").filterWhere("type", "=", "credit").as("total_credits"),
|
|
92
|
+
eb.fn.avg("rating").filterWhere("verified", "=", true).as("verified_avg_rating"),
|
|
93
|
+
])
|
|
94
|
+
.executeTakeFirst();
|
|
95
|
+
|
|
96
|
+
// .filterWhere() also works inside .having() as the first argument
|
|
97
|
+
const groupsWithNoUnsigned = await db
|
|
98
|
+
.selectFrom("document")
|
|
99
|
+
.select((eb) => [
|
|
100
|
+
"group_id",
|
|
101
|
+
eb.fn.countAll().as("total"),
|
|
102
|
+
eb.fn.countAll().filterWhere("status", "!=", "signed").as("unsigned_count"),
|
|
103
|
+
])
|
|
104
|
+
.groupBy("group_id")
|
|
105
|
+
.having(
|
|
106
|
+
(eb) => eb.fn.countAll().filterWhere("status", "!=", "signed"),
|
|
107
|
+
"=",
|
|
108
|
+
0
|
|
109
|
+
)
|
|
110
|
+
.execute();
|
|
111
|
+
|
|
112
|
+
// ============================================
|
|
113
|
+
// AGGREGATE FUNCTIONS REFERENCE
|
|
114
|
+
// ============================================
|
|
115
|
+
|
|
116
|
+
// Available aggregate functions:
|
|
117
|
+
// eb.fn.count(column) - Count rows
|
|
118
|
+
// eb.fn.countAll() - COUNT(*)
|
|
119
|
+
// eb.fn.sum(column) - Sum values
|
|
120
|
+
// eb.fn.avg(column) - Average value
|
|
121
|
+
// eb.fn.min(column) - Minimum value
|
|
122
|
+
// eb.fn.max(column) - Maximum value
|
|
123
|
+
|
|
124
|
+
// Example with all
|
|
125
|
+
const productStats = await db
|
|
126
|
+
.selectFrom("product")
|
|
127
|
+
.select((eb) => [
|
|
128
|
+
eb.fn.count("id").as("total"),
|
|
129
|
+
eb.fn.countAll().as("totalAll"),
|
|
130
|
+
eb.fn.sum("price").as("totalPrice"),
|
|
131
|
+
eb.fn.avg("price").as("avgPrice"),
|
|
132
|
+
eb.fn.min("price").as("minPrice"),
|
|
133
|
+
eb.fn.max("price").as("maxPrice"),
|
|
134
|
+
])
|
|
135
|
+
.executeTakeFirst();
|
|
136
|
+
|
|
137
|
+
// ============================================
|
|
138
|
+
// KEY PATTERNS SUMMARY
|
|
139
|
+
// ============================================
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
1. Aggregate functions return strings
|
|
143
|
+
- count, sum, avg all return string type by default
|
|
144
|
+
- Cast if you need numbers: eb.fn.count<number>("id")
|
|
145
|
+
|
|
146
|
+
2. GROUP BY rules:
|
|
147
|
+
- All non-aggregated columns in SELECT must be in GROUP BY
|
|
148
|
+
- Chain multiple .groupBy() calls for multiple columns
|
|
149
|
+
|
|
150
|
+
3. HAVING vs WHERE:
|
|
151
|
+
- WHERE: filters rows BEFORE aggregation
|
|
152
|
+
- HAVING: filters groups AFTER aggregation
|
|
153
|
+
|
|
154
|
+
4. Syntax pattern:
|
|
155
|
+
.select((eb) => [
|
|
156
|
+
"groupColumn",
|
|
157
|
+
eb.fn.count("id").as("alias"),
|
|
158
|
+
])
|
|
159
|
+
.groupBy("groupColumn")
|
|
160
|
+
.having((eb) => eb.fn.count("id"), ">", 5)
|
|
161
|
+
|
|
162
|
+
5. .filterWhere() - PostgreSQL FILTER (WHERE ...) clause:
|
|
163
|
+
- Available on ALL aggregate function builders (count, countAll, sum, avg, min, max, jsonAgg)
|
|
164
|
+
- Replaces CASE WHEN ... inside aggregates for conditional aggregation
|
|
165
|
+
- Works as the first argument to .having() for filtered aggregate conditions
|
|
166
|
+
- Example: eb.fn.count("id").filterWhere("status", "=", "active").as("active_count")
|
|
167
|
+
*/
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CTE (Common Table Expression) Patterns
|
|
3
|
+
* WITH clauses for complex multi-step queries
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// SIMPLE CTE
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
// Basic CTE - named subquery
|
|
12
|
+
const activeProducts = await db
|
|
13
|
+
.with("active_products", (db) =>
|
|
14
|
+
db
|
|
15
|
+
.selectFrom("product")
|
|
16
|
+
.select(["id", "name", "price"])
|
|
17
|
+
.where("is_active", "=", true)
|
|
18
|
+
)
|
|
19
|
+
.selectFrom("active_products")
|
|
20
|
+
.selectAll()
|
|
21
|
+
.execute();
|
|
22
|
+
|
|
23
|
+
// ============================================
|
|
24
|
+
// CTE WITH AGGREGATION
|
|
25
|
+
// ============================================
|
|
26
|
+
|
|
27
|
+
// CTE for pre-aggregating data
|
|
28
|
+
const topSpenders = await db
|
|
29
|
+
.with("order_totals", (db) =>
|
|
30
|
+
db
|
|
31
|
+
.selectFrom("order")
|
|
32
|
+
.innerJoin("user", "user.id", "order.user_id")
|
|
33
|
+
.select((eb) => [
|
|
34
|
+
"user.id as userId",
|
|
35
|
+
"user.email",
|
|
36
|
+
eb.fn.sum("order.total_amount").as("totalSpent"),
|
|
37
|
+
eb.fn.count("order.id").as("orderCount"),
|
|
38
|
+
])
|
|
39
|
+
.groupBy(["user.id", "user.email"])
|
|
40
|
+
)
|
|
41
|
+
.selectFrom("order_totals")
|
|
42
|
+
.selectAll()
|
|
43
|
+
.orderBy("totalSpent", "desc")
|
|
44
|
+
.execute();
|
|
45
|
+
|
|
46
|
+
// ============================================
|
|
47
|
+
// MULTIPLE CTEs
|
|
48
|
+
// ============================================
|
|
49
|
+
|
|
50
|
+
// Chain CTEs for multi-step processing
|
|
51
|
+
const topRatedProducts = await db
|
|
52
|
+
// First CTE: aggregate reviews
|
|
53
|
+
.with("product_reviews", (db) =>
|
|
54
|
+
db
|
|
55
|
+
.selectFrom("review")
|
|
56
|
+
.select((eb) => [
|
|
57
|
+
"product_id",
|
|
58
|
+
eb.fn.avg("rating").as("avgRating"),
|
|
59
|
+
eb.fn.count("id").as("reviewCount"),
|
|
60
|
+
])
|
|
61
|
+
.groupBy("product_id")
|
|
62
|
+
)
|
|
63
|
+
// Second CTE: join with products
|
|
64
|
+
.with("top_products", (db) =>
|
|
65
|
+
db
|
|
66
|
+
.selectFrom("product")
|
|
67
|
+
.leftJoin("product_reviews", "product_reviews.product_id", "product.id")
|
|
68
|
+
.select([
|
|
69
|
+
"product.name",
|
|
70
|
+
"product.price",
|
|
71
|
+
"product_reviews.avgRating",
|
|
72
|
+
"product_reviews.reviewCount",
|
|
73
|
+
])
|
|
74
|
+
.where("product.is_active", "=", true)
|
|
75
|
+
)
|
|
76
|
+
// Final query: select from last CTE
|
|
77
|
+
.selectFrom("top_products")
|
|
78
|
+
.selectAll()
|
|
79
|
+
.orderBy("avgRating", (ob) => ob.desc().nullsLast())
|
|
80
|
+
.execute();
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// CTE USE CASES
|
|
84
|
+
// ============================================
|
|
85
|
+
|
|
86
|
+
// Use Case 1: Break down complex logic
|
|
87
|
+
const dashboardData = await db
|
|
88
|
+
.with("monthly_orders", (db) =>
|
|
89
|
+
db
|
|
90
|
+
.selectFrom("order")
|
|
91
|
+
.select((eb) => [
|
|
92
|
+
eb.fn.count("id").as("orderCount"),
|
|
93
|
+
eb.fn.sum("total_amount").as("revenue"),
|
|
94
|
+
])
|
|
95
|
+
.where("status", "=", "completed")
|
|
96
|
+
)
|
|
97
|
+
.with("active_users", (db) =>
|
|
98
|
+
db
|
|
99
|
+
.selectFrom("user")
|
|
100
|
+
.select((eb) => eb.fn.count("id").as("userCount"))
|
|
101
|
+
.where("is_active", "=", true)
|
|
102
|
+
)
|
|
103
|
+
.selectFrom("monthly_orders")
|
|
104
|
+
.innerJoin("active_users", (join) =>
|
|
105
|
+
join.on((eb) => eb.lit(true)) // Cross join
|
|
106
|
+
)
|
|
107
|
+
.select([
|
|
108
|
+
"monthly_orders.orderCount",
|
|
109
|
+
"monthly_orders.revenue",
|
|
110
|
+
"active_users.userCount",
|
|
111
|
+
])
|
|
112
|
+
.executeTakeFirst();
|
|
113
|
+
|
|
114
|
+
// Use Case 2: Self-referencing/recursive (for hierarchies)
|
|
115
|
+
// Note: Recursive CTEs use withRecursive
|
|
116
|
+
const categoryHierarchy = await db
|
|
117
|
+
.withRecursive("category_tree", (db) =>
|
|
118
|
+
// Base case: root categories
|
|
119
|
+
db
|
|
120
|
+
.selectFrom("category")
|
|
121
|
+
.select(["id", "name", "parent_id"])
|
|
122
|
+
.where("parent_id", "is", null)
|
|
123
|
+
.unionAll(
|
|
124
|
+
// Recursive case: children
|
|
125
|
+
db
|
|
126
|
+
.selectFrom("category as c")
|
|
127
|
+
.innerJoin("category_tree as ct", "ct.id", "c.parent_id")
|
|
128
|
+
.select(["c.id", "c.name", "c.parent_id"])
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
.selectFrom("category_tree")
|
|
132
|
+
.selectAll()
|
|
133
|
+
.execute();
|
|
134
|
+
|
|
135
|
+
// ============================================
|
|
136
|
+
// KEY PATTERNS SUMMARY
|
|
137
|
+
// ============================================
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
1. Basic CTE syntax:
|
|
141
|
+
db.with("name", (db) => db.selectFrom(...))
|
|
142
|
+
.selectFrom("name")
|
|
143
|
+
.selectAll()
|
|
144
|
+
|
|
145
|
+
2. Multiple CTEs - chain .with() calls:
|
|
146
|
+
db.with("cte1", ...)
|
|
147
|
+
.with("cte2", ...) // Can reference cte1
|
|
148
|
+
.selectFrom("cte2")
|
|
149
|
+
|
|
150
|
+
3. CTEs can reference earlier CTEs:
|
|
151
|
+
- cte2 can use cte1 in its FROM
|
|
152
|
+
- Order matters!
|
|
153
|
+
|
|
154
|
+
4. Recursive CTEs:
|
|
155
|
+
- Use .withRecursive()
|
|
156
|
+
- Base case UNION ALL recursive case
|
|
157
|
+
- Recursive case joins to the CTE itself
|
|
158
|
+
|
|
159
|
+
5. When to use CTEs:
|
|
160
|
+
- Complex multi-step aggregations
|
|
161
|
+
- Reusing a subquery multiple times
|
|
162
|
+
- Breaking down complex logic
|
|
163
|
+
- Recursive/hierarchical data
|
|
164
|
+
- Improving query readability
|
|
165
|
+
*/
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression Patterns
|
|
3
|
+
* CASE, $if, subqueries, eb functions, advanced expression building
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
import { sql, expressionBuilder, Expression, SqlBool } from "kysely";
|
|
7
|
+
import type { DB } from "./db.d";
|
|
8
|
+
|
|
9
|
+
// ============================================
|
|
10
|
+
// CASE EXPRESSIONS
|
|
11
|
+
// ============================================
|
|
12
|
+
|
|
13
|
+
// Simple CASE
|
|
14
|
+
const ordersWithLabels = await db
|
|
15
|
+
.selectFrom("order")
|
|
16
|
+
.select((eb) => [
|
|
17
|
+
"id",
|
|
18
|
+
"status",
|
|
19
|
+
eb
|
|
20
|
+
.case()
|
|
21
|
+
.when("status", "=", "completed")
|
|
22
|
+
.then("Done")
|
|
23
|
+
.when("status", "=", "shipped")
|
|
24
|
+
.then("In Transit")
|
|
25
|
+
.when("status", "=", "pending")
|
|
26
|
+
.then("Processing")
|
|
27
|
+
.else("Unknown")
|
|
28
|
+
.end()
|
|
29
|
+
.as("statusLabel"),
|
|
30
|
+
])
|
|
31
|
+
.execute();
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// $if - CONDITIONAL QUERY BUILDING
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
// $if for conditional WHERE
|
|
38
|
+
const includeInactive = false;
|
|
39
|
+
const users = await db
|
|
40
|
+
.selectFrom("user")
|
|
41
|
+
.selectAll()
|
|
42
|
+
.$if(!includeInactive, (qb) => qb.where("is_active", "=", true))
|
|
43
|
+
.execute();
|
|
44
|
+
|
|
45
|
+
// $if for conditional SELECT columns
|
|
46
|
+
const includeMetadata = true;
|
|
47
|
+
const usersWithOptionalMeta = await db
|
|
48
|
+
.selectFrom("user")
|
|
49
|
+
.select(["id", "email", "first_name", "last_name"])
|
|
50
|
+
.$if(includeMetadata, (qb) => qb.select("metadata"))
|
|
51
|
+
.execute();
|
|
52
|
+
// Note: metadata becomes OPTIONAL in result type (metadata?: Json)
|
|
53
|
+
|
|
54
|
+
// Multiple $if conditions
|
|
55
|
+
const filterRole: string | null = "admin";
|
|
56
|
+
const onlyActive = true;
|
|
57
|
+
const filteredUsers = await db
|
|
58
|
+
.selectFrom("user")
|
|
59
|
+
.selectAll()
|
|
60
|
+
.$if(!!filterRole, (qb) => qb.where("role", "=", filterRole!))
|
|
61
|
+
.$if(onlyActive, (qb) => qb.where("is_active", "=", true))
|
|
62
|
+
.execute();
|
|
63
|
+
|
|
64
|
+
// ============================================
|
|
65
|
+
// SUBQUERIES
|
|
66
|
+
// ============================================
|
|
67
|
+
|
|
68
|
+
// Subquery in WHERE (IN)
|
|
69
|
+
const usersWithOrders = await db
|
|
70
|
+
.selectFrom("user")
|
|
71
|
+
.selectAll()
|
|
72
|
+
.where(
|
|
73
|
+
"id",
|
|
74
|
+
"in",
|
|
75
|
+
db.selectFrom("order").select("user_id").where("status", "=", "completed")
|
|
76
|
+
)
|
|
77
|
+
.execute();
|
|
78
|
+
|
|
79
|
+
// EXISTS subquery
|
|
80
|
+
const productsWithReviews = await db
|
|
81
|
+
.selectFrom("product")
|
|
82
|
+
.selectAll()
|
|
83
|
+
.where((eb) =>
|
|
84
|
+
eb.exists(
|
|
85
|
+
db
|
|
86
|
+
.selectFrom("review")
|
|
87
|
+
.select(sql`1`.as("one"))
|
|
88
|
+
.whereRef("review.product_id", "=", eb.ref("product.id"))
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
.execute();
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// eb.fn - FUNCTION CALLS
|
|
95
|
+
// ============================================
|
|
96
|
+
|
|
97
|
+
// String functions with eb.fn<ReturnType>
|
|
98
|
+
const userNames = await db
|
|
99
|
+
.selectFrom("user")
|
|
100
|
+
.select((eb) => [
|
|
101
|
+
eb.fn<string>("concat", [
|
|
102
|
+
eb.ref("first_name"),
|
|
103
|
+
eb.cast(eb.val(" "), "text"), // Cast string literals!
|
|
104
|
+
eb.ref("last_name"),
|
|
105
|
+
]).as("fullName"),
|
|
106
|
+
eb.fn<string>("upper", [eb.ref("email")]).as("upperEmail"),
|
|
107
|
+
eb.fn<number>("length", [eb.ref("email")]).as("emailLength"),
|
|
108
|
+
])
|
|
109
|
+
.execute();
|
|
110
|
+
|
|
111
|
+
// COALESCE
|
|
112
|
+
const productsWithDefault = await db
|
|
113
|
+
.selectFrom("product")
|
|
114
|
+
.select((eb) => [
|
|
115
|
+
"name",
|
|
116
|
+
eb.fn.coalesce("cost", sql`0`).as("costOrZero"),
|
|
117
|
+
])
|
|
118
|
+
.execute();
|
|
119
|
+
|
|
120
|
+
// ============================================
|
|
121
|
+
// BINARY EXPRESSIONS
|
|
122
|
+
// ============================================
|
|
123
|
+
|
|
124
|
+
// Arithmetic with eb()
|
|
125
|
+
const lineItems = await db
|
|
126
|
+
.selectFrom("order_item")
|
|
127
|
+
.select((eb) => [
|
|
128
|
+
"id",
|
|
129
|
+
"quantity",
|
|
130
|
+
"unit_price",
|
|
131
|
+
eb("quantity", "*", eb.ref("unit_price")).as("lineTotal"),
|
|
132
|
+
])
|
|
133
|
+
.execute();
|
|
134
|
+
|
|
135
|
+
// ============================================
|
|
136
|
+
// eb.val() vs eb.lit()
|
|
137
|
+
// ============================================
|
|
138
|
+
|
|
139
|
+
// eb.val() - Parameterized value ($1, $2) - SAFER for user input
|
|
140
|
+
// Note: May need cast for type inference
|
|
141
|
+
const searchTerm = "alice";
|
|
142
|
+
const searchResults = await db
|
|
143
|
+
.selectFrom("user")
|
|
144
|
+
.select(["id", "email"])
|
|
145
|
+
.where("email", "like", (eb) =>
|
|
146
|
+
eb.fn<string>("concat", [
|
|
147
|
+
eb.cast(eb.val("%"), "text"),
|
|
148
|
+
eb.cast(eb.val(searchTerm), "text"),
|
|
149
|
+
eb.cast(eb.val("%"), "text"),
|
|
150
|
+
])
|
|
151
|
+
)
|
|
152
|
+
.execute();
|
|
153
|
+
|
|
154
|
+
// eb.lit() - Literal in SQL - ONLY for numbers, booleans, null
|
|
155
|
+
// THROWS "unsafe immediate value" for strings!
|
|
156
|
+
const priceCategories = await db
|
|
157
|
+
.selectFrom("product")
|
|
158
|
+
.select((eb) => [
|
|
159
|
+
"name",
|
|
160
|
+
"price",
|
|
161
|
+
eb
|
|
162
|
+
.case()
|
|
163
|
+
.when("price", ">", eb.lit(100)) // Number literal - OK
|
|
164
|
+
.then(sql<string>`'expensive'`) // String - use sql``
|
|
165
|
+
.when("price", ">", eb.lit(50)) // Number literal - OK
|
|
166
|
+
.then(sql<string>`'moderate'`) // String - use sql``
|
|
167
|
+
.else(sql<string>`'cheap'`) // String - use sql``
|
|
168
|
+
.end()
|
|
169
|
+
.as("priceCategory"),
|
|
170
|
+
])
|
|
171
|
+
.execute();
|
|
172
|
+
|
|
173
|
+
// ============================================
|
|
174
|
+
// eb.not() - NEGATION
|
|
175
|
+
// ============================================
|
|
176
|
+
|
|
177
|
+
// Negate a condition
|
|
178
|
+
const inactiveProducts = await db
|
|
179
|
+
.selectFrom("product")
|
|
180
|
+
.select(["name", "is_active"])
|
|
181
|
+
.where((eb) => eb.not(eb("is_active", "=", true)))
|
|
182
|
+
.execute();
|
|
183
|
+
|
|
184
|
+
// NOT EXISTS
|
|
185
|
+
const usersWithoutOrders = await db
|
|
186
|
+
.selectFrom("user")
|
|
187
|
+
.select(["id", "email"])
|
|
188
|
+
.where((eb) =>
|
|
189
|
+
eb.not(
|
|
190
|
+
eb.exists(
|
|
191
|
+
db
|
|
192
|
+
.selectFrom("order")
|
|
193
|
+
.select(sql`1`.as("one"))
|
|
194
|
+
.whereRef("order.user_id", "=", "user.id")
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
.execute();
|
|
199
|
+
|
|
200
|
+
// ============================================
|
|
201
|
+
// STANDALONE expressionBuilder
|
|
202
|
+
// ============================================
|
|
203
|
+
|
|
204
|
+
// Build expressions outside query context
|
|
205
|
+
const eb = expressionBuilder<DB, "user">();
|
|
206
|
+
const activeCondition = eb("is_active", "=", true);
|
|
207
|
+
const adminCondition = eb("role", "=", "admin");
|
|
208
|
+
|
|
209
|
+
const activeAdmins = await db
|
|
210
|
+
.selectFrom("user")
|
|
211
|
+
.select(["id", "email", "role"])
|
|
212
|
+
.where((qb) => qb.and([activeCondition, adminCondition]))
|
|
213
|
+
.execute();
|
|
214
|
+
|
|
215
|
+
// ============================================
|
|
216
|
+
// CONDITIONAL EXPRESSIONS WITH ARRAYS
|
|
217
|
+
// ============================================
|
|
218
|
+
|
|
219
|
+
// Build conditions dynamically
|
|
220
|
+
const conditions: Expression<SqlBool>[] = [];
|
|
221
|
+
const productEb = expressionBuilder<DB, "product">();
|
|
222
|
+
|
|
223
|
+
const filterActive = true;
|
|
224
|
+
const minPrice = 50;
|
|
225
|
+
const maxPrice: number | null = null;
|
|
226
|
+
|
|
227
|
+
if (filterActive) {
|
|
228
|
+
conditions.push(productEb("is_active", "=", true));
|
|
229
|
+
}
|
|
230
|
+
if (minPrice) {
|
|
231
|
+
conditions.push(productEb("price", ">=", String(minPrice)));
|
|
232
|
+
}
|
|
233
|
+
if (maxPrice) {
|
|
234
|
+
conditions.push(productEb("price", "<=", String(maxPrice)));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const filteredProducts = await db
|
|
238
|
+
.selectFrom("product")
|
|
239
|
+
.select(["name", "price", "is_active"])
|
|
240
|
+
.$if(conditions.length > 0, (qb) => qb.where((eb) => eb.and(conditions)))
|
|
241
|
+
.execute();
|
|
242
|
+
|
|
243
|
+
// ============================================
|
|
244
|
+
// KEY PATTERNS SUMMARY
|
|
245
|
+
// ============================================
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
1. CASE expressions:
|
|
249
|
+
eb.case().when(...).then(...).else(...).end()
|
|
250
|
+
|
|
251
|
+
2. $if for conditional query parts:
|
|
252
|
+
.$if(condition, (qb) => qb.where/select/etc)
|
|
253
|
+
- Columns added via $if become OPTIONAL in type
|
|
254
|
+
|
|
255
|
+
3. eb.val() vs eb.lit():
|
|
256
|
+
- eb.val(): Parameterized ($1) - use for user input
|
|
257
|
+
- eb.lit(): Literal - ONLY numbers, booleans, null
|
|
258
|
+
- For string literals: use sql`'string'`
|
|
259
|
+
|
|
260
|
+
4. eb.not(): Negate any expression
|
|
261
|
+
eb.not(eb("col", "=", value))
|
|
262
|
+
eb.not(eb.exists(...))
|
|
263
|
+
|
|
264
|
+
5. Standalone expressionBuilder:
|
|
265
|
+
const eb = expressionBuilder<DB, "table">();
|
|
266
|
+
- Build expressions outside query context
|
|
267
|
+
- Useful for dynamic condition building
|
|
268
|
+
|
|
269
|
+
6. Expression<SqlBool>[] arrays:
|
|
270
|
+
- Build conditions dynamically
|
|
271
|
+
- Combine with eb.and([...]) or eb.or([...])
|
|
272
|
+
*/
|