@gallopsystems/agent-skills 1.14.0 → 1.16.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.
@@ -0,0 +1,132 @@
1
+ ## Common Pitfalls to Avoid
2
+
3
+ ### 1. Don't Resort to `sql`` When Kysely Has a Method
4
+
5
+ ```typescript
6
+ // WRONG
7
+ .select(sql`count(*)`.as("count"))
8
+
9
+ // RIGHT
10
+ .select((eb) => eb.fn.countAll().as("count"))
11
+
12
+ // WRONG - raw SQL for FILTER (WHERE ...) on aggregates
13
+ .having(sql<number>`count(*) filter (where status != 'signed')`, "=", 0)
14
+
15
+ // RIGHT - .filterWhere() works on all aggregate function builders
16
+ .having(
17
+ (eb) => eb.fn.countAll().filterWhere("status", "!=", "signed"),
18
+ "=",
19
+ 0
20
+ )
21
+ ```
22
+
23
+ ### 2. Don't Forget .execute()
24
+
25
+ Queries are lazy - they won't run without calling an execute method:
26
+
27
+ ```typescript
28
+ // This does nothing!
29
+ db.selectFrom("user").selectAll();
30
+
31
+ // This runs the query
32
+ await db.selectFrom("user").selectAll().execute();
33
+ ```
34
+
35
+ ### 3. Use whereRef for Column-to-Column Comparisons
36
+
37
+ ```typescript
38
+ // WRONG - Compares to string literal "other.column"
39
+ .where("table.column", "=", "other.column")
40
+
41
+ // RIGHT - Compares to actual column value
42
+ .whereRef("table.column", "=", "other.column")
43
+ ```
44
+
45
+ ### 4. Type Your Function Returns
46
+
47
+ ```typescript
48
+ // Better type inference
49
+ eb.fn<string>("concat", [...])
50
+ eb.fn<number>("length", [...])
51
+ ```
52
+
53
+ ### 5. PostgreSQL Does NOT Auto-Index Foreign Keys
54
+
55
+ Always create indexes on foreign key columns:
56
+
57
+ ```typescript
58
+ await db.schema.createIndex("idx_order_user_id").on("order").column("user_id").execute();
59
+ ```
60
+
61
+ ### 6. Always Type `sql` Template Literals
62
+
63
+ 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:
64
+
65
+ ```typescript
66
+ // WRONG - Returns unknown type
67
+ eb.fn.coalesce("some_json_col", sql`'{}'::jsonb`)
68
+
69
+ // RIGHT - Explicit type annotation
70
+ eb.fn.coalesce("some_json_col", sql<Record<string, unknown>>`'{}'::jsonb`)
71
+
72
+ // For complex types (e.g., JSON column from a CTE), use typeof with eb.ref
73
+ // This ensures the fallback type matches the column type exactly
74
+ eb.fn
75
+ .coalesce(
76
+ eb.ref("jobs_agg.jobs"),
77
+ sql<typeof eb.ref<"jobs_agg.jobs">>`'[]'::json`
78
+ )
79
+ .as("jobs")
80
+ ```
81
+
82
+ **Key rule**: Every `sql` template literal should have a type parameter: `sql<TYPE>`. This ensures proper type inference throughout your query chain.
83
+
84
+ ### 7. DATE Columns Cause Timezone Issues
85
+
86
+ By default, the `pg` driver converts DATE columns to JavaScript `Date` objects. This causes timezone problems:
87
+
88
+ ```
89
+ Database: 2025-01-01 (just a date, no time)
90
+ JS Date: 2025-01-01T00:00:00.000Z (interpreted as UTC midnight)
91
+ User in NYC sees: Dec 31, 2024 (5 hours behind UTC)
92
+ ```
93
+
94
+ **Solution: Parse DATE as string and let the frontend handle formatting**
95
+
96
+ Step 1: Configure `pg` to return DATE as string:
97
+
98
+ ```typescript
99
+ import pg from "pg";
100
+
101
+ // Tell pg to return DATE columns as strings instead of Date objects
102
+ const DATE_OID = 1082;
103
+ pg.types.setTypeParser(DATE_OID, (val: string) => val);
104
+ ```
105
+
106
+ Step 2: Update `kysely-codegen` to generate matching types:
107
+
108
+ ```bash
109
+ npx kysely-codegen \
110
+ --url="$DATABASE_URL" \
111
+ --out-file=server/db/db.d.ts \
112
+ --dialect=postgres \
113
+ --date-parser=string
114
+ ```
115
+
116
+ Now DATE columns return strings like `"2025-01-01"` and the frontend can parse/format respecting the user's timezone.
117
+
118
+ **Note**: This applies to DATE columns only. TIMESTAMPTZ columns already handle timezones correctly by storing UTC and converting on read.
119
+
120
+ ### 8. Don't Use the `between` String Operator — It Emits Invalid SQL
121
+
122
+ The `"between"` operator looks like it should work but compiles to a tuple, which is a PostgreSQL syntax error:
123
+
124
+ ```typescript
125
+ // WRONG - compiles to: "age" between ($1, $2) -> Postgres syntax error
126
+ .where("age", "between", [18, 65])
127
+
128
+ // RIGHT - use the expression-builder helpers
129
+ .where((eb) => eb.between("age", 18, 65)) // "age" between $1 and $2
130
+ .where((eb) => eb.betweenSymmetric("age", 65, 18)) // swaps bounds if needed
131
+ ```
132
+
@@ -0,0 +1,39 @@
1
+ ## Handling "Excessively Deep Types" Error
2
+
3
+ ### The Problem
4
+
5
+ Complex queries with many CTEs can overwhelm TypeScript's type instantiation limits:
6
+
7
+ ```
8
+ Type instantiation is excessively deep and possibly infinite
9
+ ```
10
+
11
+ This commonly occurs with 12+ `with` clauses, as Kysely's nested helper types accumulate.
12
+
13
+ ### The Solution: `$assertType`
14
+
15
+ Use `$assertType` to simplify the type chain at intermediate points:
16
+
17
+ ```typescript
18
+ const result = await db
19
+ .with("cte1", (qb) =>
20
+ qb.selectFrom("user")
21
+ .select(["id", "email"])
22
+ .$assertType<{ id: number; email: string }>() // Simplify type here
23
+ )
24
+ .with("cte2", (qb) =>
25
+ qb.selectFrom("cte1")
26
+ .select("email")
27
+ .$assertType<{ email: string }>()
28
+ )
29
+ // ... more CTEs
30
+ .selectFrom("cteN")
31
+ .selectAll()
32
+ .execute();
33
+ ```
34
+
35
+ **Key points**:
36
+ - The asserted type must structurally match the actual type (full type safety preserved)
37
+ - Apply to several intermediate `with` clauses in large queries
38
+ - TypeScript cannot automatically simplify these types - explicit assertion is required
39
+
@@ -0,0 +1,132 @@
1
+ ## ExpressionBuilder (eb) - The Foundation
2
+
3
+ The `eb` parameter in select/where callbacks provides all expression methods:
4
+
5
+ ```typescript
6
+ .select((eb) => [
7
+ eb.ref("column").as("alias"), // Column reference
8
+ eb.fn<string>("upper", [eb.ref("email")]), // Function call (typed!)
9
+ eb.fn.count("id").as("count"), // Aggregate function
10
+ eb.fn.sum("amount").as("total"), // SUM
11
+ eb.fn.avg("rating").as("avgRating"), // AVG
12
+ eb.fn.coalesce("nullable_col", eb.val(0)), // COALESCE
13
+ eb.case().when("status", "=", "active") // CASE expression
14
+ .then("Active").else("Inactive").end(),
15
+ eb("quantity", "*", eb.ref("unit_price")), // Binary expression
16
+ eb.exists(subquery), // EXISTS
17
+ eb.not(expression), // NOT / negation
18
+ eb.cast(eb.val(" "), "text"), // Cast value to type
19
+ eb.and([...]), // AND conditions
20
+ eb.or([...]), // OR conditions
21
+ ])
22
+ ```
23
+
24
+ ### eb.val() vs eb.lit()
25
+
26
+ ```typescript
27
+ // eb.val() - Creates a parameterized value ($1, $2, etc.) - PREFERRED for user input
28
+ // Note: eb.val() alone may fail with "could not determine data type of parameter"
29
+ // Use eb.cast(eb.val(...), "text") for string values in function arguments
30
+ eb.val("user input") // Becomes: $1 with parameter "user input"
31
+ eb.cast(eb.val("safe"), "text") // Becomes: $1::text - always works
32
+
33
+ // eb.lit() - Creates a literal value in SQL
34
+ // ONLY accepts: numbers, booleans, null - NOT strings (throws "unsafe immediate value")
35
+ eb.lit(1) // Becomes: 1 (directly in SQL)
36
+ eb.lit(true) // Becomes: true
37
+ eb.lit(null) // Becomes: NULL
38
+
39
+ // For string literals, use sql`` template instead
40
+ sql`'active'` // Becomes: 'active' (directly in SQL)
41
+ sql<string>`'label'` // Typed string literal
42
+ ```
43
+
44
+ ### Standalone ExpressionBuilder
45
+
46
+ For reusable helpers outside query callbacks:
47
+
48
+ ```typescript
49
+ import { expressionBuilder } from "kysely";
50
+ import type { DB } from "./db.d.ts";
51
+
52
+ // Create standalone expression builder
53
+ const eb = expressionBuilder<DB, "user">();
54
+
55
+ // Use in helper functions
56
+ function isActiveUser() {
57
+ return eb.and([
58
+ eb("is_active", "=", true),
59
+ eb("role", "!=", "banned"),
60
+ ]);
61
+ }
62
+ ```
63
+
64
+ A predicate helper can also return a callback that receives the query's
65
+ `eb`, so the same helper works in any `.where()`/`.having()` on that table:
66
+
67
+ ```typescript
68
+ import type { ExpressionBuilder } from "kysely";
69
+
70
+ // AVOID - raw sql means the column name is an unchecked string.
71
+ // A typo or renamed column only fails at runtime.
72
+ function nameMatches(name: string) {
73
+ return sql<boolean>`lower(name) = ${name.toLowerCase()}`;
74
+ }
75
+
76
+ // PREFER - eb.fn keeps the column reference type-checked against DB,
77
+ // and the value stays parameterized.
78
+ function nameMatches(name: string) {
79
+ return (eb: ExpressionBuilder<DB, "user">) =>
80
+ eb(eb.fn("lower", ["name"]), "=", name.toLowerCase());
81
+ }
82
+
83
+ // Both call sites stay identical:
84
+ db.selectFrom("user").where(nameMatches(input)).execute();
85
+ ```
86
+
87
+ ### Conditional Expressions with Arrays
88
+
89
+ Build dynamic filters by collecting expressions:
90
+
91
+ ```typescript
92
+ .where((eb) => {
93
+ const filters: Expression<SqlBool>[] = [];
94
+
95
+ if (firstName) filters.push(eb("first_name", "=", firstName));
96
+ if (lastName) filters.push(eb("last_name", "=", lastName));
97
+ if (minAge) filters.push(eb("age", ">=", minAge));
98
+
99
+ // Combine all filters with AND (empty array = no filter)
100
+ return eb.and(filters);
101
+ })
102
+ ```
103
+
104
+ ## String Concatenation
105
+
106
+ Use the `||` operator with `sql` template for clean string concatenation:
107
+
108
+ ```typescript
109
+ // RECOMMENDED - Clean and type-safe with eb.ref()
110
+ .select((eb) => [
111
+ sql<string>`${eb.ref("first_name")} || ' ' || ${eb.ref("last_name")}`.as("full_name"),
112
+ ])
113
+ // Output: "first_name" || ' ' || "last_name"
114
+
115
+ // ALTERNATIVE - Pure eb() chaining (parameterized literals)
116
+ .select((eb) => [
117
+ eb(eb("first_name", "||", " "), "||", eb.ref("last_name")).as("full_name"),
118
+ ])
119
+ // Output: "first_name" || $1 || "last_name"
120
+
121
+ // VERBOSE - concat() function (avoid unless you need NULL handling)
122
+ .select((eb) => [
123
+ eb.fn<string>("concat", [
124
+ eb.ref("first_name"),
125
+ eb.cast(eb.val(" "), "text"),
126
+ eb.ref("last_name"),
127
+ ]).as("full_name"),
128
+ ])
129
+ ```
130
+
131
+ **Note**: `concat()` treats NULL as empty string, while `||` propagates NULL. Use `concat()` only when you need that NULL behavior.
132
+
@@ -0,0 +1,26 @@
1
+ ## Full-Text Search (PostgreSQL)
2
+
3
+ `@@` is a real Kysely operator — keep it as the operator and put the FTS functions on each side as `sql` fragments. See [full-text-search.ts](references/full-text-search.ts).
4
+
5
+ ```typescript
6
+ db.selectFrom("document").selectAll()
7
+ .where(
8
+ sql`to_tsvector('english', ${sql.ref("body")})`,
9
+ "@@",
10
+ sql`websearch_to_tsquery('english', ${userInput})`,
11
+ )
12
+ .execute();
13
+ // A stored tsvector column can be the typed LHS directly:
14
+ // .where("search_vector", "@@", sql`plainto_tsquery('english', ${userInput})`)
15
+ ```
16
+
17
+ ## Advanced Grouping (ROLLUP / CUBE / GROUPING SETS)
18
+
19
+ No builder exists — pass the grouping spec to `.groupBy()` as a `sql` fragment; the SELECT list stays typed. See [aggregations.ts](references/aggregations.ts).
20
+
21
+ ```typescript
22
+ .groupBy(sql`rollup("status")`) // hierarchical subtotals
23
+ .groupBy(sql`cube("order_id", "product_id")`) // all combinations
24
+ .groupBy(sql`grouping sets (("status"), ("user_id"), ())`) // explicit sets
25
+ ```
26
+
@@ -0,0 +1,81 @@
1
+ ## PostgreSQL Helpers Summary
2
+
3
+ All helpers from `kysely/helpers/postgres`:
4
+
5
+ ```typescript
6
+ import {
7
+ jsonArrayFrom, // One-to-many relations (subquery → array)
8
+ jsonObjectFrom, // Many-to-one relations (subquery → object | null)
9
+ jsonBuildObject, // Build JSON object from expressions
10
+ mergeAction, // Get action performed in MERGE query (PostgreSQL 15+)
11
+ } from "kysely/helpers/postgres";
12
+ ```
13
+
14
+ **Note**: `jsonAgg` is NOT imported - use `eb.fn.jsonAgg()` instead.
15
+
16
+ ### mergeAction (PostgreSQL 15+)
17
+
18
+ For MERGE queries, get which action was performed:
19
+
20
+ ```typescript
21
+ import { mergeAction } from "kysely/helpers/postgres";
22
+
23
+ const result = await db
24
+ .mergeInto("person")
25
+ .using("person_updates", "person.id", "person_updates.id")
26
+ .whenMatched()
27
+ .thenUpdateSet({ name: eb.ref("person_updates.name") })
28
+ .whenNotMatched()
29
+ .thenInsertValues({ id: eb.ref("person_updates.id"), name: eb.ref("person_updates.name") })
30
+ .returning([mergeAction().as("action"), "id"])
31
+ .execute();
32
+
33
+ // result[0].action is 'INSERT' | 'UPDATE' | 'DELETE'
34
+ ```
35
+
36
+ ## Extending Kysely
37
+
38
+ ### Custom Helper Functions
39
+
40
+ Most extensions use the `sql` template tag with `RawBuilder<T>`:
41
+
42
+ ```typescript
43
+ import { sql, RawBuilder } from "kysely";
44
+
45
+ // Create a typed helper function
46
+ function json<T>(value: T): RawBuilder<T> {
47
+ return sql`CAST(${JSON.stringify(value)} AS JSONB)`;
48
+ }
49
+
50
+ // Use in queries
51
+ .select((eb) => [
52
+ json({ name: "value" }).as("data"),
53
+ ])
54
+ ```
55
+
56
+ ### Custom Expression Classes
57
+
58
+ For reusable expressions, implement the `Expression<T>` interface:
59
+
60
+ ```typescript
61
+ import { Expression, OperationNode, sql } from "kysely";
62
+
63
+ class JsonValue<T> implements Expression<T> {
64
+ readonly #value: T;
65
+
66
+ constructor(value: T) {
67
+ this.#value = value;
68
+ }
69
+
70
+ get expressionType(): T | undefined {
71
+ return undefined;
72
+ }
73
+
74
+ toOperationNode(): OperationNode {
75
+ return sql`CAST(${JSON.stringify(this.#value)} AS JSONB)`.toOperationNode();
76
+ }
77
+ }
78
+ ```
79
+
80
+ **Note**: Module augmentation and inheritance-based extension are not recommended.
81
+