Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import {
2 type InsertQueryBuilder,
3 type Selection,
4 type SelectQueryBuilder,
5 type SelectType,
6} from 'kysely';
7import { type IfAny, type Simplify, type UnionToIntersection } from 'type-fest';
8
9import { type PickEach } from './typescript-types.js';
10
11export const isUniqueViolationError = isPgErrorWithCode.bind(null, '23505');
12export const isForeignKeyViolationError = isPgErrorWithCode.bind(null, '23503');
13export const isNotNullViolationError = isPgErrorWithCode.bind(null, '23502');
14export const isCheckViolationError = isPgErrorWithCode.bind(null, '23514');
15
16// See https://github.com/postgres/postgres/blob/eb81e8e7902f63c4d292638edc8b7e92b766a692/src/backend/utils/errcodes.txt#L227
17function isPgErrorWithCode(code: string, error: unknown) {
18 return (
19 typeof error === 'object' &&
20 error !== null &&
21 (error as { code?: unknown }).code === code
22 );
23}
24
25type SelectionToAliasMap<T extends string> = Simplify<
26 UnionToIntersection<
27 T extends `${infer RawColumn} as ${infer Alias}`
28 ? { [K in RawColumn]: Alias }
29 : { [K in T]: K }
30 >
31>;
32
33type PickEachNoAliasing<
34 RowType,
35 SelectionType extends readonly string[],
36> = PickEach<
37 RowType,
38 keyof SelectionToAliasMap<SelectionType[number]> & keyof RowType
39>;
40
41type ApplyAliases<
42 UnaliasedRowType,
43 SelectionType extends readonly string[],
44> = UnaliasedRowType extends object
45 ? SelectionToAliasMap<SelectionType[number]> extends {
46 [k: string]: string;
47 }
48 ? ApplyAlias<UnaliasedRowType, SelectionToAliasMap<SelectionType[number]>>
49 : never
50 : never;
51
52type ApplyAlias<T extends object, AliasMap extends { [k: string]: string }> = {
53 [K in keyof T as AliasMap[K & keyof AliasMap]]: SelectType<T[K]>;
54};
55
56/**
57 * Kysely represents the type of each row as an object type (with the ability to
58 * have different types per column on SELECT/INSERT/UPDATE). However, if certain
59 * fields have correlated types (e.g., if column `a` can be type `X | Y` and
60 * column `b` can be type `A | B`, but column `a` having type `X` implies column
61 * `b` must have type `Y`), kysely has no way to capture this in query results,
62 * because it computes the type of the returned selection (i.e., portion of a
63 * row) on a column-by-column basis.
64 *
65 * This type is solely designed to work around that problem. You give it:
66 *
67 * - RowTypeUnion: a type for your row which contains a union type to capture
68 * the correlation between fields;
69 * - SelectionType: a list of column names (optionally with aliases) matching
70 * exactly what you'd pass to `builder.select()` in kysely. This could also be
71 * a list of keys (with aliases and possibly partial) that are going to
72 * represent the row in an insert/update.
73 * - ConditionalSelectionType: a kysely query can have columns that are _only
74 * sometimes_ selected using $if() calls. This type should be a list of column
75 * names (optionally with aliases) matching the conditional selection. See
76 * https://github.com/kysely-org/kysely/blob/e4de7bb8f7f22ad5d7af72dfe0285eb7a85cdd9a/site/docs/recipes/conditional-selects.md
77 * - Mode: an indication of whether the produced type is supposed to represent
78 * the shape of the data as it's needed in an insert, update, or select.
79 *
80 * Then, it returns a union type, with only the keys in the selection and with
81 * their aliases applied, for the row.
82 */
83export type FixKyselyRowCorrelation<
84 RowTypeUnion,
85 SelectionType extends readonly string[],
86 ConditionalSelectionType extends readonly string[] = [],
87> = Simplify<
88 ApplyAliases<PickEachNoAliasing<RowTypeUnion, SelectionType>, SelectionType> &
89 Partial<
90 ApplyAliases<
91 PickEachNoAliasing<RowTypeUnion, ConditionalSelectionType>,
92 ConditionalSelectionType
93 >
94 >
95>;
96
97// When a kysely select query includes a $if() call, the type of the
98// SelectQueryBuilder's third parameter, which reflects the shape of the query's
99// returned rows, is set by Kysely to be:
100// `MergePartial<RequiredSelection, SelectionWhenTheIfConditionIsTrue>`.
101// In the FixSingleTableSelectRowType, we need to extract these two selections,
102// so we duplicate Kysely's definition of MergePartial here so that we can use
103// it FixSingleTableSelectRowType.
104type MergePartial<T1, T2> = T1 & Partial<Omit<T2, keyof T1>>;
105
106/**
107 * A small abstraction over `FixKyselyRowCorrelation` that only works for SELECT
108 * queries on single tables (no joins; no subqueries) or INSERT queries with
109 * `RETURNING`, but that saves boilerplate in those cases, by taking the type of
110 * the whole `SelectQueryBuilder`/`InsertQueryBuilder` as it's only required
111 * parameter.
112 */
113export type FixSingleTableReturnedRowType<
114 // prettier-ignore
115 Builder extends
116 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
117 SelectQueryBuilder<any, any, Selection<any, any, any>>
118 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
119 InsertQueryBuilder<any, any, Selection<any, any, any>>,
120 WhereClause = unknown,
121> =
122 // We need to destructure the two selections out of the MergePartial in order
123 // to properly track that the second set of columns are optional in the
124 // result. See `MergePartial` and https://github.com/roostorg/coop/pull/1248
125 Builder extends SelectQueryBuilder<
126 infer DB,
127 infer TB,
128 MergePartial<
129 // eslint-disable-next-line @typescript-eslint/no-explicit-any
130 infer Part1 extends Selection<any, any, any>,
131 // eslint-disable-next-line @typescript-eslint/no-explicit-any
132 infer Part2 extends Selection<any, any, any>
133 >
134 >
135 ? Part1 extends Selection<DB, TB, infer Sel1>
136 ? Part2 extends Selection<DB, TB, infer Sel2>
137 ? FixKyselyRowCorrelation<
138 DB[TB] & WhereClause,
139 readonly (Sel1 & string)[],
140 IfAny<Sel2, readonly [], readonly (Sel2 & string)[]>
141 >
142 : never
143 : never
144 : Builder extends
145 | SelectQueryBuilder<
146 infer DB,
147 infer TB,
148 Selection<infer DB, infer TB, infer SelectionType>
149 >
150 | InsertQueryBuilder<
151 infer DB,
152 infer TB,
153 Selection<infer DB, infer TB, infer SelectionType>
154 >
155 ? FixKyselyRowCorrelation<
156 DB[TB] & WhereClause,
157 readonly (SelectionType & string)[],
158 []
159 >
160 : never;
161
162/**
163 * Creates an object with a key, where the type of the value for that key
164 * excludes a given value.
165 *
166 * E.g., `Excluding<{ a: 'foo' | 'bar' }, 'a', 'foo'>` is `{ a: 'bar' }`.
167 *
168 * K can be a union type to exclude V from all keys in the union.
169 *
170 * Similarly, V can be a union type to exclude all values in the union, assuming
171 * that the values being excluded are legal values for every given key.
172 */
173export type Excluding<O extends object, K extends keyof O, V extends O[K]> = {
174 [K2 in K]: Exclude<O[K2], V>;
175};