@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,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation Patterns
|
|
3
|
+
* INSERT, UPDATE, DELETE, UPSERT
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
import { sql } from "kysely";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// INSERT
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
// Basic insert with RETURNING
|
|
13
|
+
const newCategory = await db
|
|
14
|
+
.insertInto("category")
|
|
15
|
+
.values({ name: "New Category", sort_order: 99 })
|
|
16
|
+
.returning(["id", "name"])
|
|
17
|
+
.executeTakeFirst();
|
|
18
|
+
|
|
19
|
+
// Insert multiple rows
|
|
20
|
+
const newCategories = await db
|
|
21
|
+
.insertInto("category")
|
|
22
|
+
.values([
|
|
23
|
+
{ name: "Category A", sort_order: 100 },
|
|
24
|
+
{ name: "Category B", sort_order: 101 },
|
|
25
|
+
])
|
|
26
|
+
.returning(["id", "name"])
|
|
27
|
+
.execute();
|
|
28
|
+
|
|
29
|
+
// ============================================
|
|
30
|
+
// UPSERT (ON CONFLICT)
|
|
31
|
+
// ============================================
|
|
32
|
+
|
|
33
|
+
// Insert or update if conflict
|
|
34
|
+
const upsertedProduct = await db
|
|
35
|
+
.insertInto("product")
|
|
36
|
+
.values({
|
|
37
|
+
name: "Upsert Product",
|
|
38
|
+
sku: "UPSERT-001",
|
|
39
|
+
price: "99.99",
|
|
40
|
+
stock_quantity: 10,
|
|
41
|
+
})
|
|
42
|
+
.onConflict((oc) =>
|
|
43
|
+
oc.column("sku").doUpdateSet((eb) => ({
|
|
44
|
+
// Update these columns on conflict - type-safe!
|
|
45
|
+
stock_quantity: eb("product.stock_quantity", "+", eb.ref("excluded.stock_quantity")),
|
|
46
|
+
}))
|
|
47
|
+
)
|
|
48
|
+
.returning(["id", "sku", "stock_quantity"])
|
|
49
|
+
.executeTakeFirst();
|
|
50
|
+
|
|
51
|
+
// OnConflict variations:
|
|
52
|
+
// - oc.column("col") - single column constraint
|
|
53
|
+
// - oc.columns(["col1", "col2"]) - composite constraint
|
|
54
|
+
// - oc.constraint("constraint_name") - named constraint
|
|
55
|
+
// - oc.doNothing() - ignore conflicts
|
|
56
|
+
// - oc.doUpdateSet({...}) - update on conflict
|
|
57
|
+
|
|
58
|
+
// Upsert with doNothing
|
|
59
|
+
await db
|
|
60
|
+
.insertInto("user")
|
|
61
|
+
.values({ email: "exists@example.com", first_name: "Test", last_name: "User" })
|
|
62
|
+
.onConflict((oc) => oc.column("email").doNothing())
|
|
63
|
+
.execute();
|
|
64
|
+
|
|
65
|
+
// ============================================
|
|
66
|
+
// UPDATE
|
|
67
|
+
// ============================================
|
|
68
|
+
|
|
69
|
+
// Basic update
|
|
70
|
+
const updatedCategory = await db
|
|
71
|
+
.updateTable("category")
|
|
72
|
+
.set({ sort_order: 999 })
|
|
73
|
+
.where("name", "=", "New Category")
|
|
74
|
+
.returning(["id", "name", "sort_order"])
|
|
75
|
+
.executeTakeFirst();
|
|
76
|
+
|
|
77
|
+
// Update with expression (increment)
|
|
78
|
+
const updatedStock = await db
|
|
79
|
+
.updateTable("product")
|
|
80
|
+
.set((eb) => ({
|
|
81
|
+
stock_quantity: eb("stock_quantity", "+", 5),
|
|
82
|
+
}))
|
|
83
|
+
.where("sku", "=", "UPSERT-001")
|
|
84
|
+
.returning(["id", "sku", "stock_quantity"])
|
|
85
|
+
.executeTakeFirst();
|
|
86
|
+
|
|
87
|
+
// Update multiple columns with expressions
|
|
88
|
+
await db
|
|
89
|
+
.updateTable("product")
|
|
90
|
+
.set((eb) => ({
|
|
91
|
+
stock_quantity: eb("stock_quantity", "-", 1),
|
|
92
|
+
price: eb("price", "*", eb.lit(1.1)), // 10% increase
|
|
93
|
+
}))
|
|
94
|
+
.where("id", "=", 1)
|
|
95
|
+
.execute();
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// DELETE
|
|
99
|
+
// ============================================
|
|
100
|
+
|
|
101
|
+
// Delete with RETURNING
|
|
102
|
+
const deleted = await db
|
|
103
|
+
.deleteFrom("category")
|
|
104
|
+
.where("name", "like", "Category%")
|
|
105
|
+
.returning(["id", "name"])
|
|
106
|
+
.execute();
|
|
107
|
+
|
|
108
|
+
// Delete with subquery
|
|
109
|
+
await db
|
|
110
|
+
.deleteFrom("order_item")
|
|
111
|
+
.where(
|
|
112
|
+
"order_id",
|
|
113
|
+
"in",
|
|
114
|
+
db.selectFrom("order").select("id").where("status", "=", "cancelled")
|
|
115
|
+
)
|
|
116
|
+
.execute();
|
|
117
|
+
|
|
118
|
+
// ============================================
|
|
119
|
+
// INSERT FROM SELECT
|
|
120
|
+
// ============================================
|
|
121
|
+
|
|
122
|
+
// Insert rows from another query
|
|
123
|
+
const auditLogs = await db
|
|
124
|
+
.insertInto("inventory_log")
|
|
125
|
+
.columns(["product_id", "change_quantity", "reason"])
|
|
126
|
+
.expression(
|
|
127
|
+
db
|
|
128
|
+
.selectFrom("product")
|
|
129
|
+
.select([
|
|
130
|
+
"id as product_id",
|
|
131
|
+
sql`0`.as("change_quantity"),
|
|
132
|
+
sql`'Audit check'`.as("reason"),
|
|
133
|
+
])
|
|
134
|
+
.where("is_active", "=", true)
|
|
135
|
+
.limit(10)
|
|
136
|
+
)
|
|
137
|
+
.returning(["id", "product_id", "reason"])
|
|
138
|
+
.execute();
|
|
139
|
+
|
|
140
|
+
// ============================================
|
|
141
|
+
// UNION
|
|
142
|
+
// ============================================
|
|
143
|
+
|
|
144
|
+
// Combine results from multiple queries
|
|
145
|
+
const contacts = await db
|
|
146
|
+
.selectFrom("user")
|
|
147
|
+
.select(["email as contact", sql`'email'`.as("type")])
|
|
148
|
+
.where("role", "=", "admin")
|
|
149
|
+
.union(
|
|
150
|
+
db
|
|
151
|
+
.selectFrom("user")
|
|
152
|
+
.select(["email as contact", sql`'email'`.as("type")])
|
|
153
|
+
.where("role", "=", "manager")
|
|
154
|
+
)
|
|
155
|
+
.execute();
|
|
156
|
+
|
|
157
|
+
// unionAll - includes duplicates
|
|
158
|
+
// intersect - rows in both queries
|
|
159
|
+
// except - rows in first but not second
|
|
160
|
+
|
|
161
|
+
// ============================================
|
|
162
|
+
// KEY PATTERNS SUMMARY
|
|
163
|
+
// ============================================
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
1. INSERT:
|
|
167
|
+
.insertInto("table")
|
|
168
|
+
.values({...}) or .values([...])
|
|
169
|
+
.returning([...])
|
|
170
|
+
.execute() or .executeTakeFirst()
|
|
171
|
+
|
|
172
|
+
2. UPSERT (ON CONFLICT) - type-safe with eb callback:
|
|
173
|
+
.onConflict((oc) => oc.column("col").doUpdateSet((eb) => ({
|
|
174
|
+
col: eb("table.col", "+", eb.ref("excluded.col")),
|
|
175
|
+
})))
|
|
176
|
+
- eb.ref("excluded.col") for inserted values
|
|
177
|
+
- eb("table.col", ...) for existing values (type-safe!)
|
|
178
|
+
|
|
179
|
+
3. UPDATE:
|
|
180
|
+
.updateTable("table")
|
|
181
|
+
.set({...}) or .set((eb) => ({...}))
|
|
182
|
+
.where(...)
|
|
183
|
+
- Use eb(col, op, value) for expressions
|
|
184
|
+
|
|
185
|
+
4. DELETE:
|
|
186
|
+
.deleteFrom("table")
|
|
187
|
+
.where(...)
|
|
188
|
+
.returning([...]) - optional
|
|
189
|
+
|
|
190
|
+
5. INSERT FROM SELECT:
|
|
191
|
+
.insertInto("table")
|
|
192
|
+
.columns([...])
|
|
193
|
+
.expression(db.selectFrom(...))
|
|
194
|
+
|
|
195
|
+
6. RETURNING:
|
|
196
|
+
- Available on INSERT, UPDATE, DELETE
|
|
197
|
+
- Returns affected rows
|
|
198
|
+
- Use with executeTakeFirst() for single row
|
|
199
|
+
*/
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORDER BY and Pagination Patterns
|
|
3
|
+
* Sorting, NULLS handling, DISTINCT, pagination
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// ORDER BY
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
// Simple ORDER BY
|
|
12
|
+
const productsByPrice = await db
|
|
13
|
+
.selectFrom("product")
|
|
14
|
+
.select(["name", "price"])
|
|
15
|
+
.orderBy("price", "desc")
|
|
16
|
+
.execute();
|
|
17
|
+
|
|
18
|
+
// Multiple ORDER BY - chain calls
|
|
19
|
+
// NOT array syntax - that's deprecated!
|
|
20
|
+
const sortedProducts = await db
|
|
21
|
+
.selectFrom("product")
|
|
22
|
+
.select(["name", "price", "stock_quantity"])
|
|
23
|
+
.orderBy("is_active", "desc") // Primary sort
|
|
24
|
+
.orderBy("price", "asc") // Secondary sort
|
|
25
|
+
.execute();
|
|
26
|
+
|
|
27
|
+
// ============================================
|
|
28
|
+
// NULLS FIRST / NULLS LAST
|
|
29
|
+
// ============================================
|
|
30
|
+
|
|
31
|
+
// Use order builder callback (ob) - NOT sql``!
|
|
32
|
+
const productsNullsLast = await db
|
|
33
|
+
.selectFrom("product")
|
|
34
|
+
.select(["name", "category_id"])
|
|
35
|
+
.orderBy("category_id", (ob) => ob.asc().nullsLast())
|
|
36
|
+
.execute();
|
|
37
|
+
|
|
38
|
+
const productsNullsFirst = await db
|
|
39
|
+
.selectFrom("product")
|
|
40
|
+
.select(["name", "category_id"])
|
|
41
|
+
.orderBy("category_id", (ob) => ob.desc().nullsFirst())
|
|
42
|
+
.execute();
|
|
43
|
+
|
|
44
|
+
// ============================================
|
|
45
|
+
// PAGINATION
|
|
46
|
+
// ============================================
|
|
47
|
+
|
|
48
|
+
// LIMIT and OFFSET
|
|
49
|
+
const page2 = await db
|
|
50
|
+
.selectFrom("product")
|
|
51
|
+
.selectAll()
|
|
52
|
+
.orderBy("name")
|
|
53
|
+
.limit(10) // Items per page
|
|
54
|
+
.offset(10) // Skip first page (page 1)
|
|
55
|
+
.execute();
|
|
56
|
+
|
|
57
|
+
// Pagination helper pattern
|
|
58
|
+
async function getPaginatedProducts(page: number, pageSize: number) {
|
|
59
|
+
return db
|
|
60
|
+
.selectFrom("product")
|
|
61
|
+
.selectAll()
|
|
62
|
+
.orderBy("created_at", "desc")
|
|
63
|
+
.limit(pageSize)
|
|
64
|
+
.offset((page - 1) * pageSize)
|
|
65
|
+
.execute();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================
|
|
69
|
+
// DISTINCT
|
|
70
|
+
// ============================================
|
|
71
|
+
|
|
72
|
+
// DISTINCT - unique values
|
|
73
|
+
const uniqueStatuses = await db
|
|
74
|
+
.selectFrom("order")
|
|
75
|
+
.select("status")
|
|
76
|
+
.distinct()
|
|
77
|
+
.execute();
|
|
78
|
+
|
|
79
|
+
// DISTINCT ON (PostgreSQL only)
|
|
80
|
+
// Get first row for each unique value of specified column(s)
|
|
81
|
+
const latestOrderPerUser = await db
|
|
82
|
+
.selectFrom("order")
|
|
83
|
+
.distinctOn("user_id")
|
|
84
|
+
.select(["user_id", "id", "status", "created_at"])
|
|
85
|
+
.orderBy("user_id")
|
|
86
|
+
.orderBy("created_at", "desc") // Most recent first
|
|
87
|
+
.execute();
|
|
88
|
+
// Returns one row per user - their latest order
|
|
89
|
+
|
|
90
|
+
// ============================================
|
|
91
|
+
// KEY PATTERNS SUMMARY
|
|
92
|
+
// ============================================
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
1. ORDER BY syntax:
|
|
96
|
+
.orderBy("column", "asc") // ascending
|
|
97
|
+
.orderBy("column", "desc") // descending
|
|
98
|
+
|
|
99
|
+
2. Multiple columns - CHAIN, don't use array:
|
|
100
|
+
.orderBy("col1", "desc")
|
|
101
|
+
.orderBy("col2", "asc")
|
|
102
|
+
|
|
103
|
+
3. NULLS handling - use order builder callback:
|
|
104
|
+
.orderBy("col", (ob) => ob.asc().nullsLast())
|
|
105
|
+
.orderBy("col", (ob) => ob.desc().nullsFirst())
|
|
106
|
+
|
|
107
|
+
AVOID sql`` for this - deprecated pattern!
|
|
108
|
+
|
|
109
|
+
4. Pagination:
|
|
110
|
+
.limit(pageSize)
|
|
111
|
+
.offset((page - 1) * pageSize)
|
|
112
|
+
|
|
113
|
+
5. DISTINCT ON (PostgreSQL):
|
|
114
|
+
- Gets first row per unique value
|
|
115
|
+
- ORDER BY must start with DISTINCT ON column(s)
|
|
116
|
+
- Then order by what determines "first"
|
|
117
|
+
*/
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relations Patterns
|
|
3
|
+
* jsonArrayFrom and jsonObjectFrom for nested data
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// IMPORTANT: Kysely is NOT an ORM
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
// Kysely doesn't have built-in relations like Prisma or TypeORM.
|
|
13
|
+
// Instead, use PostgreSQL JSON functions to fetch related data
|
|
14
|
+
// in a single query.
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// jsonArrayFrom - One-to-Many
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
// User with their orders (one user -> many orders)
|
|
21
|
+
const usersWithOrders = await db
|
|
22
|
+
.selectFrom("user")
|
|
23
|
+
.select((eb) => [
|
|
24
|
+
"user.id",
|
|
25
|
+
"user.email",
|
|
26
|
+
"user.first_name",
|
|
27
|
+
// Correlated subquery returns JSON array
|
|
28
|
+
jsonArrayFrom(
|
|
29
|
+
eb
|
|
30
|
+
.selectFrom("order")
|
|
31
|
+
.select(["order.id", "order.status", "order.total_amount"])
|
|
32
|
+
.whereRef("order.user_id", "=", "user.id") // Correlation!
|
|
33
|
+
.orderBy("order.created_at", "desc")
|
|
34
|
+
).as("orders"),
|
|
35
|
+
])
|
|
36
|
+
.where("user.is_active", "=", true)
|
|
37
|
+
.execute();
|
|
38
|
+
|
|
39
|
+
// Result type: { id, email, first_name, orders: Array<{id, status, total_amount}> }
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// jsonObjectFrom - Many-to-One
|
|
43
|
+
// ============================================
|
|
44
|
+
|
|
45
|
+
// Product with its category (many products -> one category)
|
|
46
|
+
const productsWithCategory = await db
|
|
47
|
+
.selectFrom("product")
|
|
48
|
+
.select((eb) => [
|
|
49
|
+
"product.id",
|
|
50
|
+
"product.name",
|
|
51
|
+
"product.price",
|
|
52
|
+
// Correlated subquery returns single JSON object
|
|
53
|
+
jsonObjectFrom(
|
|
54
|
+
eb
|
|
55
|
+
.selectFrom("category")
|
|
56
|
+
.select(["category.id", "category.name"])
|
|
57
|
+
.whereRef("category.id", "=", "product.category_id") // Correlation!
|
|
58
|
+
).as("category"),
|
|
59
|
+
])
|
|
60
|
+
.execute();
|
|
61
|
+
|
|
62
|
+
// Result type: { id, name, price, category: {id, name} | null }
|
|
63
|
+
|
|
64
|
+
// ============================================
|
|
65
|
+
// COMBINED NESTED RELATIONS
|
|
66
|
+
// ============================================
|
|
67
|
+
|
|
68
|
+
// Deep nesting: User -> Orders -> Products
|
|
69
|
+
const userOrderDetails = await db
|
|
70
|
+
.selectFrom("user")
|
|
71
|
+
.select((eb) => [
|
|
72
|
+
"user.id",
|
|
73
|
+
"user.email",
|
|
74
|
+
jsonArrayFrom(
|
|
75
|
+
eb
|
|
76
|
+
.selectFrom("order")
|
|
77
|
+
.innerJoin("order_item", "order_item.order_id", "order.id")
|
|
78
|
+
.innerJoin("product", "product.id", "order_item.product_id")
|
|
79
|
+
.select([
|
|
80
|
+
"order.id as orderId",
|
|
81
|
+
"order.status",
|
|
82
|
+
"product.name as productName",
|
|
83
|
+
"order_item.quantity",
|
|
84
|
+
])
|
|
85
|
+
.whereRef("order.user_id", "=", "user.id")
|
|
86
|
+
).as("orderDetails"),
|
|
87
|
+
])
|
|
88
|
+
.where("user.email", "=", "alice@example.com")
|
|
89
|
+
.executeTakeFirst();
|
|
90
|
+
|
|
91
|
+
// ============================================
|
|
92
|
+
// MULTIPLE RELATIONS
|
|
93
|
+
// ============================================
|
|
94
|
+
|
|
95
|
+
// Order with user (many-to-one) AND items (one-to-many)
|
|
96
|
+
const ordersComplete = await db
|
|
97
|
+
.selectFrom("order")
|
|
98
|
+
.select((eb) => [
|
|
99
|
+
"order.id",
|
|
100
|
+
"order.status",
|
|
101
|
+
"order.total_amount",
|
|
102
|
+
// Many-to-one: user
|
|
103
|
+
jsonObjectFrom(
|
|
104
|
+
eb
|
|
105
|
+
.selectFrom("user")
|
|
106
|
+
.select(["user.id", "user.email", "user.first_name"])
|
|
107
|
+
.whereRef("user.id", "=", "order.user_id")
|
|
108
|
+
).as("user"),
|
|
109
|
+
// One-to-many: items
|
|
110
|
+
jsonArrayFrom(
|
|
111
|
+
eb
|
|
112
|
+
.selectFrom("order_item")
|
|
113
|
+
.innerJoin("product", "product.id", "order_item.product_id")
|
|
114
|
+
.select([
|
|
115
|
+
"order_item.quantity",
|
|
116
|
+
"order_item.unit_price",
|
|
117
|
+
"product.name as productName",
|
|
118
|
+
])
|
|
119
|
+
.whereRef("order_item.order_id", "=", "order.id")
|
|
120
|
+
).as("items"),
|
|
121
|
+
])
|
|
122
|
+
.execute();
|
|
123
|
+
|
|
124
|
+
// ============================================
|
|
125
|
+
// WITH FILTERING AND ORDERING
|
|
126
|
+
// ============================================
|
|
127
|
+
|
|
128
|
+
// Only include active items, ordered by price
|
|
129
|
+
const productWithActiveReviews = await db
|
|
130
|
+
.selectFrom("product")
|
|
131
|
+
.select((eb) => [
|
|
132
|
+
"product.id",
|
|
133
|
+
"product.name",
|
|
134
|
+
jsonArrayFrom(
|
|
135
|
+
eb
|
|
136
|
+
.selectFrom("review")
|
|
137
|
+
.select(["review.id", "review.rating", "review.title"])
|
|
138
|
+
.whereRef("review.product_id", "=", "product.id")
|
|
139
|
+
.where("review.rating", ">=", 4) // Filter inside relation!
|
|
140
|
+
.orderBy("review.created_at", "desc")
|
|
141
|
+
.limit(5) // Limit inside relation!
|
|
142
|
+
).as("topReviews"),
|
|
143
|
+
])
|
|
144
|
+
.execute();
|
|
145
|
+
|
|
146
|
+
// ============================================
|
|
147
|
+
// KEY PATTERNS SUMMARY
|
|
148
|
+
// ============================================
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
1. jsonArrayFrom - One-to-Many:
|
|
152
|
+
- Returns JSON array of related rows
|
|
153
|
+
- Use whereRef() to correlate with outer query
|
|
154
|
+
- Can include joins, filters, ordering inside
|
|
155
|
+
|
|
156
|
+
2. jsonObjectFrom - Many-to-One:
|
|
157
|
+
- Returns single JSON object or null
|
|
158
|
+
- Use whereRef() to correlate with outer query
|
|
159
|
+
- Returns null if no match (like LEFT JOIN)
|
|
160
|
+
|
|
161
|
+
3. Key differences from ORMs:
|
|
162
|
+
- No automatic eager/lazy loading
|
|
163
|
+
- You explicitly define what to fetch
|
|
164
|
+
- Single query, no N+1 problem
|
|
165
|
+
- Full control over the SQL
|
|
166
|
+
|
|
167
|
+
4. whereRef() is critical:
|
|
168
|
+
- Links subquery to outer query
|
|
169
|
+
- First arg: inner table column
|
|
170
|
+
- Third arg: outer table column (eb.ref())
|
|
171
|
+
|
|
172
|
+
5. Performance benefits:
|
|
173
|
+
- Single database round trip
|
|
174
|
+
- PostgreSQL optimizes the JSON aggregation
|
|
175
|
+
- No N+1 queries ever
|
|
176
|
+
*/
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SELECT and WHERE Patterns
|
|
3
|
+
* Basic query patterns for fetching and filtering data
|
|
4
|
+
*/
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
import { sql } from "kysely";
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// SELECT PATTERNS
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
// Select all columns
|
|
13
|
+
const allUsers = await db.selectFrom("user").selectAll().execute();
|
|
14
|
+
|
|
15
|
+
// Select specific columns
|
|
16
|
+
const userNames = await db
|
|
17
|
+
.selectFrom("user")
|
|
18
|
+
.select(["id", "email", "first_name", "last_name"])
|
|
19
|
+
.execute();
|
|
20
|
+
|
|
21
|
+
// Column aliases with eb.ref().as()
|
|
22
|
+
const aliasedUsers = await db
|
|
23
|
+
.selectFrom("user")
|
|
24
|
+
.select((eb) => [
|
|
25
|
+
eb.ref("first_name").as("firstName"),
|
|
26
|
+
eb.ref("last_name").as("lastName"),
|
|
27
|
+
eb.ref("email").as("emailAddress"),
|
|
28
|
+
])
|
|
29
|
+
.execute();
|
|
30
|
+
|
|
31
|
+
// executeTakeFirst - returns T | undefined (for 0 or 1 row)
|
|
32
|
+
const maybeUser = await db
|
|
33
|
+
.selectFrom("user")
|
|
34
|
+
.selectAll()
|
|
35
|
+
.where("email", "=", "alice@example.com")
|
|
36
|
+
.executeTakeFirst();
|
|
37
|
+
|
|
38
|
+
// executeTakeFirstOrThrow - throws if no row found
|
|
39
|
+
const definiteUser = await db
|
|
40
|
+
.selectFrom("user")
|
|
41
|
+
.selectAll()
|
|
42
|
+
.where("email", "=", "alice@example.com")
|
|
43
|
+
.executeTakeFirstOrThrow();
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// WHERE CLAUSES
|
|
47
|
+
// ============================================
|
|
48
|
+
|
|
49
|
+
// Equality
|
|
50
|
+
const admins = await db
|
|
51
|
+
.selectFrom("user")
|
|
52
|
+
.selectAll()
|
|
53
|
+
.where("role", "=", "admin")
|
|
54
|
+
.execute();
|
|
55
|
+
|
|
56
|
+
// Comparison operators (<, >, <=, >=, !=)
|
|
57
|
+
const expensiveProducts = await db
|
|
58
|
+
.selectFrom("product")
|
|
59
|
+
.select(["name", "price"])
|
|
60
|
+
.where("price", ">", "100")
|
|
61
|
+
.execute();
|
|
62
|
+
|
|
63
|
+
// IN clause - array of values
|
|
64
|
+
const pendingOrShipped = await db
|
|
65
|
+
.selectFrom("order")
|
|
66
|
+
.selectAll()
|
|
67
|
+
.where("status", "in", ["pending", "shipped"])
|
|
68
|
+
.execute();
|
|
69
|
+
|
|
70
|
+
// LIKE pattern matching
|
|
71
|
+
const bookProducts = await db
|
|
72
|
+
.selectFrom("product")
|
|
73
|
+
.select(["name", "sku"])
|
|
74
|
+
.where("name", "like", "%Book%")
|
|
75
|
+
.execute();
|
|
76
|
+
|
|
77
|
+
// IS NULL / IS NOT NULL
|
|
78
|
+
const uncategorized = await db
|
|
79
|
+
.selectFrom("product")
|
|
80
|
+
.select(["name", "category_id"])
|
|
81
|
+
.where("category_id", "is", null)
|
|
82
|
+
.execute();
|
|
83
|
+
|
|
84
|
+
// ============================================
|
|
85
|
+
// COMBINING CONDITIONS
|
|
86
|
+
// ============================================
|
|
87
|
+
|
|
88
|
+
// Multiple WHERE = AND (chained)
|
|
89
|
+
// Chaining .where() creates AND conditions
|
|
90
|
+
const activeAffordable = await db
|
|
91
|
+
.selectFrom("product")
|
|
92
|
+
.select(["name", "price", "is_active"])
|
|
93
|
+
.where("is_active", "=", true)
|
|
94
|
+
.where("price", "<", "100")
|
|
95
|
+
.execute();
|
|
96
|
+
|
|
97
|
+
// OR conditions using eb.or()
|
|
98
|
+
const adminOrManager = await db
|
|
99
|
+
.selectFrom("user")
|
|
100
|
+
.selectAll()
|
|
101
|
+
.where((eb) =>
|
|
102
|
+
eb.or([
|
|
103
|
+
eb("role", "=", "admin"),
|
|
104
|
+
eb("role", "=", "manager"),
|
|
105
|
+
])
|
|
106
|
+
)
|
|
107
|
+
.execute();
|
|
108
|
+
|
|
109
|
+
// Complex AND/OR with eb.and() and eb.or()
|
|
110
|
+
const complexFilter = await db
|
|
111
|
+
.selectFrom("product")
|
|
112
|
+
.select(["name", "price", "stock_quantity"])
|
|
113
|
+
.where((eb) =>
|
|
114
|
+
eb.and([
|
|
115
|
+
eb("is_active", "=", true),
|
|
116
|
+
eb.or([
|
|
117
|
+
eb("price", "<", "50"),
|
|
118
|
+
eb("stock_quantity", ">", 100),
|
|
119
|
+
]),
|
|
120
|
+
])
|
|
121
|
+
)
|
|
122
|
+
.execute();
|
|
123
|
+
|
|
124
|
+
// ============================================
|
|
125
|
+
// KEY PATTERNS SUMMARY
|
|
126
|
+
// ============================================
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
1. selectAll() vs select([...])
|
|
130
|
+
- selectAll() gets all columns
|
|
131
|
+
- select([...]) for specific columns - better performance
|
|
132
|
+
|
|
133
|
+
2. executeTakeFirst() vs execute()
|
|
134
|
+
- execute() returns array
|
|
135
|
+
- executeTakeFirst() returns single row or undefined
|
|
136
|
+
- executeTakeFirstOrThrow() throws if not found
|
|
137
|
+
|
|
138
|
+
3. WHERE chaining = AND
|
|
139
|
+
- .where(...).where(...) creates AND
|
|
140
|
+
- Use eb.or([...]) for OR
|
|
141
|
+
- Use eb.and([...]) for explicit AND
|
|
142
|
+
|
|
143
|
+
4. eb() inside where callbacks
|
|
144
|
+
- eb("column", "=", value) creates comparison
|
|
145
|
+
- Returns Expression<SqlBool> for composability
|
|
146
|
+
*/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "linear",
|
|
3
|
+
"description": "Create, triage, and manage Linear issues at Gallop Systems following team conventions: cycle placement, issue templates, project / milestone hierarchy, project refresh, and cycle rebalance. Includes a bash GraphQL CLI for the operations the Linear MCP server doesn't expose.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Gallop Systems"
|
|
7
|
+
}
|
|
8
|
+
}
|