1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
/**
* @see https://gist.github.com/rphlmr/0d1722a794ed5a16da0fdf6652902b15
*/
import { type QueryBuilderOpts } from "@/types/table"
import {
and,
is,
not,
sql,
type AnyColumn,
type Column,
type GetColumnData,
type SelectedFields,
type SQL,
type Table,
} from "drizzle-orm"
import { pgTableCreator, PgTimestampString } from "drizzle-orm/pg-core"
import { type SelectResultFields } from "drizzle-orm/query-builders/select.types"
import { databasePrefix } from "@/lib/constants"
/**
* This lets us use the multi-project schema feature of Drizzle ORM. So the same
* database instance can be used for multiple projects.
*
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
*/
export const pgTable = pgTableCreator((name) => `${databasePrefix}_${name}`)
/**
* Takes the first item from an array.
*
* @param items - The array to take the first item from.
* @returns The first item from the array.
*/
export function takeFirst<TData>(items: TData[]) {
return items.at(0)
}
/**
* Takes the first item from an array or returns null if the array is empty.
*
* @param items - The array to take the first item from.
* @returns The first item from the array or null.
*/
export function takeFirstOrNull<TData>(items: TData[]) {
return takeFirst(items) ?? null
}
/**
* Takes the first item from an array or throws an error if the array is empty.
*
* @param items - The array to take the first item from.
* @returns The first item from the array.
* @throws An error if the array is empty.
*/
export function takeFirstOrThrow<TData>(items: TData[]) {
const first = takeFirst(items)
if (!first) {
throw new Error("First item not found")
}
return first
}
/**
* Coalesces a value to a default value.
*
* @param value - The value to coalesce.
* @param defaultValue - The default value to return if the value is null.
* @returns A sql coalesce statement.
*/
export function coalesce<TData>(
value: SQL.Aliased<TData> | SQL<TData>,
defaultValue: SQL<TData>
) {
return sql<TData>`coalesce(${value}, ${defaultValue})`
}
/**
* Builds a json object from a shape.
*
* @param shape - The shape of the object to build.
* @returns A sql json_build_object statement.
*/
export function jsonBuildObject<TFields extends SelectedFields<Column, Table>>(
shape: TFields
) {
const chunks: SQL[] = []
Object.entries(shape).forEach(([key, value]) => {
if (chunks.length > 0) {
chunks.push(sql.raw(`,`))
}
chunks.push(sql.raw(`'${key}',`))
// json_build_object formats to ISO 8601 ...
if (is(value, PgTimestampString)) {
chunks.push(sql`timezone('UTC', ${value})`)
} else {
chunks.push(sql`${value}`)
}
})
return sql<
SelectResultFields<TFields>
>`json_build_object(${sql.join(chunks)})`
}
/**
* ggregates an array of values into a single json array.
*
* @param shape - The shape of the object to aggregate.
* @param options - The options to pass to the aggregate function.
* @returns A SQL json_agg statement.
*/
export function jsonAgg<Column extends AnyColumn>(
column: Column,
opts?: QueryBuilderOpts
) {
const orderBy = opts?.orderBy ? sql` order by ${opts.orderBy}` : sql``
const aggregateFunction = opts?.distinct
? sql`json_agg(distinct ${sql`${column}`} ${orderBy})`
: sql`json_agg(${sql`${column}`} ${orderBy})`
const where = opts?.nullish ? sql`true` : sql`${column} is not null`
return coalesce<GetColumnData<Column, "raw">[]>(
sql`${aggregateFunction} filter (where ${where})`,
sql`'[]'::json`
)
}
/**
* Aggregates an array of objects into a single json object.
*
* @param shape - The shape of the object to build.
* @param options - The options to pass to the aggregate function.
* @returns A SQL json_agg statement.
*/
export function jsonAggBuildObject<
TFields extends SelectedFields<Column, Table>,
Column extends AnyColumn,
>(shape: TFields, opts?: Omit<QueryBuilderOpts, "distinct">) {
const nullishWhere = opts?.nullish
? sql`true`
: sql`${sql.join(
Object.values(shape).map((value) => sql`${value} is not null`),
sql` and `
)}`
const orderBy = opts?.orderBy ? sql` order by ${opts.orderBy}` : sql``
const where = opts?.where ? and(opts.where, nullishWhere) : nullishWhere
return coalesce<SelectResultFields<TFields>[]>(
sql`json_agg(${jsonBuildObject(shape)}${orderBy}) filter (where ${where})`,
sql`'[]'::json`
)
}
/**
* Aggregates an array of values into a single array.
*
* @param column - The column to aggregate.
* @param opts - The options to pass to the aggregate function.
* @returns A SQL array_agg statement.
*/
export function arrayAgg<Column extends AnyColumn>(
column: Column,
opts?: QueryBuilderOpts
) {
const orderBy = opts?.orderBy ? sql` order by ${opts.orderBy}` : sql``
const aggregateFunction = opts?.distinct
? sql`array_agg(distinct ${sql`${column}`} ${orderBy})`
: sql`array_agg(${sql`${column}`} ${orderBy})`
return sql`coalesce(nullif(${aggregateFunction}, '{}'), array[]::${column.dataType}[])`
}
/**
* Combines array_agg with jsonb_build_object for more flexible aggregation.
*
* @param shape - An object representing the shape of the jsonb to build
* @param opts - Options for aggregation (distinct, where, orderBy)
* @returns A SQL statement for array_agg with jsonb_build_object
*/
export function arrayAggBuildObject<
TFields extends Record<string, AnyColumn | SQL>,
>(fields: TFields, options?: QueryBuilderOpts) {
const jsonbBuildObject = sql`jsonb_build_object(${sql.join(
Object.entries(fields).flatMap(([key, value]) => [
sql.raw(`'${key}'`),
value,
]),
sql`, `
)})`
const distinctClause = options?.distinct ? sql`distinct ` : sql``
const orderByClause = options?.orderBy
? sql` order by ${options.orderBy}`
: sql``
const whereClause = options?.where
? sql` filter (where ${options.where})`
: sql``
return sql<SelectResultFields<TFields>[]>`
coalesce(
array_agg(${distinctClause}${jsonbBuildObject}${orderByClause})${whereClause},
array[]::jsonb[]
)
`
}
/**
* Creates a SQL case statement for provided cases and an else value.
*
* @param cases - An array of objects with a `when` property and a `then` property.
* @param elseValue - The value to return if no case matches.
* @returns A SQL case statement.
*/
export function caseWhen<TColumn extends Column>(
cases: { when: SQL | undefined; then: Column }[],
elseValue: TColumn
) {
const chunks: SQL[] = []
cases.forEach(({ when, then }) => {
chunks.push(sql`when ${when} then ${then}`)
})
chunks.push(sql`else ${elseValue}`)
return sql<TColumn["_"]["dataType"]>`case ${sql.join(chunks)} end`
}
/**
* Composes an array of columns into a single st.
*
* @param values - The values to compose.
* @param separator - The separator to use between the ids (default: "").
* @returns A sql statement representing the composite column.
*/
export function compose<TColumn extends Column>(
columns: TColumn[],
separator = ""
) {
const chunks = columns.map((column) => sql`${column}::text`)
return sql<
TColumn["_"]["dataType"]
>`(${sql.join(chunks, sql` || ${separator} || `)})`
}
/**
* Checks if a value is empty (null, undefined, empty string, empty array, or empty object).
*
* @param value - The value to check.
* @returns A SQL expression that evaluates to true if the value is empty, false otherwise.
*/
export function isEmpty<TColumn extends AnyColumn>(column: TColumn) {
return sql<boolean>`
case
when ${column} is null then true
when ${column} = '' then true
when ${column}::text = '[]' then true
when ${column}::text = '{}' then true
else false
end
`
}
/**
* Checks if a value is not empty (not null, not undefined, not empty string, not empty array, and not empty object).
*
* @param value - The value to check.
* @returns A SQL expression that evaluates to true if the value is not empty, false otherwise.
*/
export function isNotEmpty<TColumn extends AnyColumn>(column: TColumn) {
return not(isEmpty(column))
}
|