wabe-postgres 0.5.1 → 0.5.2

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/src/index.ts ADDED
@@ -0,0 +1,752 @@
1
+ import { Pool } from 'pg'
2
+ import type {
3
+ AdapterOptions,
4
+ DatabaseAdapter,
5
+ GetObjectOptions,
6
+ CreateObjectOptions,
7
+ UpdateObjectOptions,
8
+ GetObjectsOptions,
9
+ CreateObjectsOptions,
10
+ UpdateObjectsOptions,
11
+ DeleteObjectsOptions,
12
+ WhereType,
13
+ DeleteObjectOptions,
14
+ OutputType,
15
+ CountOptions,
16
+ OrderType,
17
+ WabeTypes,
18
+ TypeField,
19
+ SchemaInterface,
20
+ } from 'wabe'
21
+
22
+ const getSQLColumnCreateTableFromType = <T extends WabeTypes>(type: TypeField<T>) => {
23
+ switch (type.type) {
24
+ case 'String':
25
+ return `TEXT${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
26
+ case 'Int':
27
+ return `INT${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
28
+ case 'Float':
29
+ return `FLOAT${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
30
+ case 'Boolean':
31
+ return `BOOLEAN${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
32
+ case 'Email':
33
+ return `VARCHAR(255)${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
34
+ case 'Phone':
35
+ return `VARCHAR(255)${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
36
+ case 'Date':
37
+ // Because we store date in iso string in database
38
+ return `VARCHAR(255)${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
39
+ case 'File':
40
+ return `JSONB${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
41
+ case 'Object':
42
+ return `JSONB${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
43
+ case 'Array':
44
+ return `JSONB${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
45
+ case 'Pointer':
46
+ return `VARCHAR(255)${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
47
+ case 'Relation':
48
+ return `JSONB${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
49
+ default:
50
+ // Enum or scalar
51
+ return `VARCHAR(255)${type.required ? ' NOT NULL' : ' DEFAULT NULL'}`
52
+ }
53
+ }
54
+
55
+ export const buildPostgresOrderQuery = <
56
+ T extends WabeTypes,
57
+ K extends keyof T['types'],
58
+ U extends keyof T['types'][K],
59
+ >(
60
+ order?: OrderType<T, K, U>,
61
+ ): string => {
62
+ if (!order) return ''
63
+
64
+ const objectKeys = Object.keys(order) as Array<keyof OrderType<T, K, U>>
65
+
66
+ if (objectKeys.length === 0) return ''
67
+
68
+ const orderClauses = objectKeys.map(
69
+ (key) => `${key === 'id' ? '_id' : String(key)} ${order[key]}`,
70
+ )
71
+
72
+ return `ORDER BY ${orderClauses.join(', ')}`
73
+ }
74
+
75
+ export const buildPostgresWhereQueryAndValues = <T extends WabeTypes, K extends keyof T['types']>(
76
+ where?: WhereType<T, K>,
77
+ startParamIndex = 1,
78
+ parentKey?: string,
79
+ ): { query: string; values: any[]; paramIndex: number } => {
80
+ if (!where) return { query: '', values: [], paramIndex: startParamIndex }
81
+
82
+ const objectKeys = Object.keys(where) as Array<keyof WhereType<T, K>>
83
+
84
+ if (objectKeys.length === 0) return { query: '', values: [], paramIndex: startParamIndex }
85
+
86
+ const acc = objectKeys.reduce(
87
+ (acc, key) => {
88
+ const value = where[key]
89
+ const keyToWrite = key === 'id' ? '_id' : String(key)
90
+
91
+ const fullKey = parentKey ? `${parentKey}->>'${keyToWrite}'` : `"${keyToWrite}"`
92
+
93
+ const simpleFullKey = parentKey ? `${parentKey}->'${keyToWrite}'` : `"${keyToWrite}"`
94
+
95
+ if ('equalTo' in (value || {})) {
96
+ if (value?.equalTo === null || value?.equalTo === undefined) {
97
+ acc.conditions.push(`${fullKey} IS NULL`)
98
+ return acc
99
+ }
100
+
101
+ acc.conditions.push(`${fullKey} = $${acc.paramIndex}`)
102
+ acc.values.push(
103
+ Array.isArray(value.equalTo) ? JSON.stringify(value.equalTo) : value.equalTo,
104
+ )
105
+ acc.paramIndex++
106
+ return acc
107
+ }
108
+
109
+ if ('notEqualTo' in (value || {})) {
110
+ if (value?.notEqualTo === null || value?.notEqualTo === undefined) {
111
+ acc.conditions.push(`${fullKey} IS NOT NULL`)
112
+ return acc
113
+ }
114
+
115
+ acc.conditions.push(`${fullKey} IS DISTINCT FROM $${acc.paramIndex}`)
116
+ acc.values.push(
117
+ Array.isArray(value.notEqualTo) ? JSON.stringify(value.notEqualTo) : value.notEqualTo,
118
+ )
119
+ acc.paramIndex++
120
+ return acc
121
+ }
122
+
123
+ if (value?.exists === true) {
124
+ if (parentKey) {
125
+ acc.conditions.push(
126
+ `${parentKey} IS NOT NULL
127
+ AND ${parentKey} ? '${keyToWrite}'
128
+ AND ${parentKey}->'${keyToWrite}' IS NOT NULL
129
+ AND ${parentKey}->'${keyToWrite}' <> 'null'::jsonb`,
130
+ )
131
+ } else {
132
+ acc.conditions.push(`"${keyToWrite}" IS NOT NULL`)
133
+ }
134
+ return acc
135
+ }
136
+
137
+ if (value?.exists === false) {
138
+ if (parentKey) {
139
+ acc.conditions.push(
140
+ `${parentKey} IS NULL
141
+ OR NOT (${parentKey} ? '${keyToWrite}')
142
+ OR ${parentKey}->'${keyToWrite}' IS NULL
143
+ OR ${parentKey}->'${keyToWrite}' = 'null'::jsonb`,
144
+ )
145
+ } else {
146
+ acc.conditions.push(`"${keyToWrite}" IS NULL`)
147
+ }
148
+ return acc
149
+ }
150
+
151
+ if (value?.greaterThan || value?.greaterThan === null) {
152
+ acc.conditions.push(`${fullKey} > $${acc.paramIndex}`)
153
+ acc.values.push(value.greaterThan)
154
+ acc.paramIndex++
155
+ return acc
156
+ }
157
+
158
+ if (value?.greaterThanOrEqualTo || value?.greaterThanOrEqualTo === null) {
159
+ acc.conditions.push(`${fullKey} >= $${acc.paramIndex}`)
160
+ acc.values.push(value.greaterThanOrEqualTo)
161
+ acc.paramIndex++
162
+ return acc
163
+ }
164
+
165
+ if (value?.lessThan || value?.lessThan === null) {
166
+ acc.conditions.push(`${fullKey} < $${acc.paramIndex}`)
167
+ acc.values.push(value.lessThan)
168
+ acc.paramIndex++
169
+ return acc
170
+ }
171
+
172
+ if (value?.lessThanOrEqualTo || value?.lessThanOrEqualTo === null) {
173
+ acc.conditions.push(`${fullKey} <= $${acc.paramIndex}`)
174
+ acc.values.push(value.lessThanOrEqualTo)
175
+ acc.paramIndex++
176
+ return acc
177
+ }
178
+
179
+ if (value?.in && Array.isArray(value.in) && value.in.length > 0) {
180
+ const placeholders = value.in.map(() => `$${acc.paramIndex++}`).join(', ')
181
+
182
+ acc.conditions.push(`${fullKey} IN (${placeholders})`)
183
+
184
+ acc.values.push(...value.in)
185
+ return acc
186
+ }
187
+
188
+ if (value?.notIn && Array.isArray(value.notIn) && value.notIn.length > 0) {
189
+ const placeholders = value.notIn.map(() => `$${acc.paramIndex++}`).join(', ')
190
+ acc.conditions.push(`${fullKey} NOT IN (${placeholders})`)
191
+ acc.values.push(...value.notIn)
192
+ return acc
193
+ }
194
+
195
+ if (value?.contains) {
196
+ // Simple access on json field because contains is use for array or object column
197
+ acc.conditions.push(`${simpleFullKey} @> $${acc.paramIndex}`)
198
+ acc.values.push(
199
+ Array.isArray(value.contains)
200
+ ? JSON.stringify(value.contains)
201
+ : JSON.stringify([value.contains]),
202
+ )
203
+ acc.paramIndex++
204
+ return acc
205
+ }
206
+
207
+ if (value?.notContains) {
208
+ // Simple access on json field because contains is use for array or object column
209
+ acc.conditions.push(`NOT (${simpleFullKey} @> $${acc.paramIndex})`)
210
+ acc.values.push(
211
+ Array.isArray(value.notContains)
212
+ ? JSON.stringify(value.notContains)
213
+ : JSON.stringify([value.notContains]),
214
+ )
215
+ acc.paramIndex++
216
+ return acc
217
+ }
218
+
219
+ if (key === 'OR' && Array.isArray(value) && value.length > 0) {
220
+ const orConditions = value.map((orWhere) => {
221
+ const {
222
+ query,
223
+ values: orValues,
224
+ paramIndex: newParamIndex,
225
+ } = buildPostgresWhereQueryAndValues(orWhere, acc.paramIndex)
226
+ acc.paramIndex = newParamIndex
227
+ return { query, values: orValues }
228
+ })
229
+
230
+ const orQueries = orConditions.filter(({ query }) => query).map(({ query }) => `${query}`)
231
+
232
+ if (orQueries.length > 0) {
233
+ acc.conditions.push(`(${orQueries.join(' OR ')})`)
234
+
235
+ for (const orCondition of orConditions) {
236
+ acc.values.push(...orCondition.values)
237
+ }
238
+ }
239
+ return acc
240
+ }
241
+
242
+ if (key === 'AND' && Array.isArray(value) && value.length > 0) {
243
+ const andConditions = value.map((andWhere) => {
244
+ const {
245
+ query,
246
+ values: andValues,
247
+ paramIndex: newParamIndex,
248
+ } = buildPostgresWhereQueryAndValues(andWhere, acc.paramIndex)
249
+
250
+ acc.paramIndex = newParamIndex
251
+
252
+ return { query, values: andValues }
253
+ })
254
+
255
+ const andQueries = andConditions.filter(({ query }) => query).map(({ query }) => `${query}`)
256
+
257
+ if (andQueries.length > 0) {
258
+ acc.conditions.push(`(${andQueries.join(' AND ')})`)
259
+
260
+ for (const andCondition of andConditions) {
261
+ acc.values.push(...andCondition.values)
262
+ }
263
+ }
264
+ return acc
265
+ }
266
+
267
+ if (typeof value === 'object') {
268
+ const nestedResult = buildPostgresWhereQueryAndValues(
269
+ value as any,
270
+ acc.paramIndex,
271
+ simpleFullKey,
272
+ )
273
+ if (nestedResult.query) {
274
+ acc.conditions.push(`(${nestedResult.query})`)
275
+ acc.values.push(...nestedResult.values)
276
+ acc.paramIndex = nestedResult.paramIndex
277
+ }
278
+ return acc
279
+ }
280
+
281
+ return acc
282
+ },
283
+ {
284
+ conditions: [] as string[],
285
+ values: [] as any[],
286
+ paramIndex: startParamIndex,
287
+ },
288
+ )
289
+
290
+ return {
291
+ query: acc.conditions.length > 0 ? acc.conditions.join(' AND ') : '',
292
+ values: acc.values,
293
+ paramIndex: acc.paramIndex,
294
+ }
295
+ }
296
+
297
+ const computeValuesFromData = (data: Record<string, any>) => {
298
+ return Object.values(data).map((value) => {
299
+ if (Array.isArray(value)) return JSON.stringify(value)
300
+
301
+ return value
302
+ })
303
+ }
304
+
305
+ export class PostgresAdapter<T extends WabeTypes> implements DatabaseAdapter<T> {
306
+ public options: AdapterOptions
307
+ public postgresPool: Pool
308
+ public pool: Pool
309
+
310
+ constructor(options: AdapterOptions) {
311
+ this.options = options
312
+ this.postgresPool = new Pool({
313
+ connectionString: `${options.databaseUrl}/postgres`,
314
+ })
315
+ this.pool = new Pool({
316
+ // TODO: Improve this to support ending with a slash and more.
317
+ // Need to parse to detect if user already added the database name
318
+ connectionString: `${this.options.databaseUrl}/${this.options.databaseName}`,
319
+ })
320
+ }
321
+
322
+ async initializeDatabase(schema: SchemaInterface<T>) {
323
+ // We create the database with the pool on postgres database
324
+ const client = await this.postgresPool.connect()
325
+
326
+ try {
327
+ const res = await client.query('SELECT datname FROM pg_database WHERE datname = $1', [
328
+ this.options.databaseName,
329
+ ])
330
+
331
+ if (res.rowCount === 0) await client.query(`CREATE DATABASE "${this.options.databaseName}"`)
332
+
333
+ await Promise.all(
334
+ (schema.classes || []).map((classSchema) => {
335
+ return this.createClassIfNotExist(classSchema.name, schema)
336
+ }),
337
+ )
338
+ } finally {
339
+ client.release()
340
+ }
341
+ }
342
+
343
+ async clearDatabase() {
344
+ const client = await this.pool.connect()
345
+
346
+ try {
347
+ const tablesResult = await client.query(`
348
+ SELECT tablename
349
+ FROM pg_catalog.pg_tables
350
+ WHERE schemaname = 'public' AND tablename != 'Role'
351
+ `)
352
+
353
+ await Promise.all(
354
+ tablesResult.rows.map((table) =>
355
+ client.query(`TRUNCATE TABLE "${table.tablename}" CASCADE`),
356
+ ),
357
+ )
358
+ } finally {
359
+ client.release()
360
+ }
361
+ }
362
+
363
+ async close() {
364
+ if (!this.pool.ended) await this.pool.end()
365
+ if (!this.postgresPool.ended) await this.postgresPool.end()
366
+ }
367
+
368
+ async createClassIfNotExist(className: keyof T['types'], schema: SchemaInterface<T>) {
369
+ const schemaClass = schema?.classes?.find((currentClass) => currentClass.name === className)
370
+
371
+ if (!schemaClass) throw new Error(`${String(className)} is not defined in schema`)
372
+
373
+ const client = await this.pool.connect()
374
+
375
+ try {
376
+ const columns = Object.entries(schemaClass.fields)
377
+
378
+ const createTableParams = columns
379
+ .map(([fieldName, field]) => {
380
+ const sqlColumnCreateTable = getSQLColumnCreateTableFromType(field)
381
+
382
+ return `"${fieldName}" ${sqlColumnCreateTable}`
383
+ })
384
+ .join(', ')
385
+
386
+ // Create the table if it doesn't exist
387
+ await client.query(`
388
+ CREATE TABLE IF NOT EXISTS "${String(className)}" (
389
+ _id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
390
+ ${createTableParams}
391
+ )
392
+ `)
393
+
394
+ // Update the table if a column is added after the first launch
395
+ const res = await client.query(
396
+ `
397
+ SELECT column_name
398
+ FROM information_schema.columns
399
+ WHERE table_name = $1
400
+ `,
401
+ [String(className)],
402
+ )
403
+
404
+ const existingColumns = res.rows.map((row) => row.column_name)
405
+
406
+ // Add missing columns to the table
407
+ await Promise.all(
408
+ columns
409
+ .filter(([fieldName]) => !existingColumns.includes(fieldName))
410
+ .map(([fieldName, field]) => {
411
+ const sqlColumnCreateTable = getSQLColumnCreateTableFromType(field)
412
+ return client.query(`
413
+ ALTER TABLE "${String(className)}"
414
+ ADD COLUMN "${fieldName}" ${sqlColumnCreateTable}
415
+ `)
416
+ }),
417
+ )
418
+
419
+ // Create indexes if they don't exist
420
+ const indexes = schemaClass.indexes || []
421
+
422
+ await Promise.all(
423
+ indexes.map((index) => {
424
+ const indexType = index.unique ? 'UNIQUE' : ''
425
+ const indexDirection = index.order === 'ASC' ? 'ASC' : 'DESC'
426
+
427
+ return client.query(`
428
+ CREATE ${indexType} INDEX IF NOT EXISTS
429
+ idx_${String(className)}_${String(index.field)}
430
+ ON "${String(className)}" (${String(index.field)}' ${indexDirection})
431
+ `)
432
+ }),
433
+ )
434
+
435
+ return className
436
+ } finally {
437
+ client.release()
438
+ }
439
+ }
440
+
441
+ async count<K extends keyof T['types']>(params: CountOptions<T, K>) {
442
+ const { className, where } = params
443
+
444
+ const client = await this.pool.connect()
445
+
446
+ try {
447
+ const { query, values } = buildPostgresWhereQueryAndValues(where)
448
+
449
+ const whereClause = query ? `WHERE ${query}` : ''
450
+
451
+ const result = await client.query(
452
+ `SELECT COUNT(*) FROM "${String(className)}" ${whereClause}`,
453
+ values,
454
+ )
455
+
456
+ return Number(result.rows[0].count)
457
+ } finally {
458
+ client.release()
459
+ }
460
+ }
461
+
462
+ async getObject<K extends keyof T['types'], U extends keyof T['types'][K]>(
463
+ params: GetObjectOptions<T, K, U>,
464
+ ): Promise<OutputType<T, K, U>> {
465
+ const { className, id, select, where } = params
466
+
467
+ const client = await this.pool.connect()
468
+
469
+ try {
470
+ // 2 because 1 is _id
471
+ const { query, values } = buildPostgresWhereQueryAndValues(where, 2)
472
+
473
+ const whereClause = query ? `AND ${query}` : ''
474
+
475
+ const selectFields = select
476
+ ? Object.keys(select)
477
+ .filter((key) => select[key as keyof typeof select])
478
+ .map((key) => `"${key === 'id' ? '_id' : key}"`)
479
+ : []
480
+
481
+ const selectExpression = selectFields.length > 0 ? selectFields.join(', ') : '*'
482
+
483
+ const result = await client.query(
484
+ `SELECT ${selectExpression} FROM "${String(
485
+ className,
486
+ )}" WHERE _id = $1 ${whereClause} LIMIT 1`,
487
+ [id, ...values],
488
+ )
489
+
490
+ if (result.rows.length === 0) throw new Error('Object not found')
491
+
492
+ const row = result.rows[0]
493
+
494
+ const { _id, ...data } = row
495
+
496
+ return {
497
+ ...data,
498
+ id: _id,
499
+ } as OutputType<T, K, U>
500
+ } finally {
501
+ client.release()
502
+ }
503
+ }
504
+
505
+ async getObjects<
506
+ K extends keyof T['types'],
507
+ U extends keyof T['types'][K],
508
+ W extends keyof T['types'][K],
509
+ >(params: GetObjectsOptions<T, K, U, W>): Promise<OutputType<T, K, W>[]> {
510
+ const { className, select, where, offset, first, order } = params
511
+
512
+ const client = await this.pool.connect()
513
+
514
+ try {
515
+ const { query, values } = buildPostgresWhereQueryAndValues(where)
516
+
517
+ const whereClause = query ? `WHERE ${query}` : ''
518
+ const orderClause = buildPostgresOrderQuery(order)
519
+ const limitClause = first ? `LIMIT ${first}` : ''
520
+ const offsetClause = offset ? `OFFSET ${offset}` : ''
521
+
522
+ const selectFields = select
523
+ ? Object.keys(select)
524
+ .filter((key) => select[key as keyof typeof select])
525
+ .map((key) => `"${key === 'id' ? '_id' : key}"`)
526
+ : []
527
+
528
+ const selectExpression = selectFields.length > 0 ? selectFields.join(', ') : '*'
529
+
530
+ const result = await client.query(
531
+ `SELECT ${selectExpression} FROM "${String(
532
+ className,
533
+ )}" ${whereClause} ${orderClause} ${limitClause} ${offsetClause}`,
534
+ values,
535
+ )
536
+
537
+ const rows = result.rows as Array<Record<string, any>>
538
+
539
+ return rows.map((row) => {
540
+ const { _id, ...data } = row
541
+
542
+ return {
543
+ ...data,
544
+ id: _id,
545
+ } as OutputType<T, K, W>
546
+ })
547
+ } finally {
548
+ client.release()
549
+ }
550
+ }
551
+
552
+ async createObject<
553
+ K extends keyof T['types'],
554
+ U extends keyof T['types'][K],
555
+ W extends keyof T['types'][K],
556
+ >(params: CreateObjectOptions<T, K, U, W>): Promise<{ id: string }> {
557
+ const { className, data } = params
558
+
559
+ const client = await this.pool.connect()
560
+
561
+ try {
562
+ const columns = Object.keys(data).map((column) => `"${column}"`)
563
+ const values = computeValuesFromData(data)
564
+ const placeholders = columns.map((_, index) => `$${index + 1}`)
565
+
566
+ const result = await client.query(
567
+ columns.length === 0
568
+ ? `INSERT INTO "${String(className)}" DEFAULT VALUES RETURNING _id`
569
+ : `INSERT INTO "${String(className)}" (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING _id`,
570
+ values,
571
+ )
572
+
573
+ return { id: result.rows[0]._id }
574
+ } finally {
575
+ client.release()
576
+ }
577
+ }
578
+
579
+ async createObjects<
580
+ K extends keyof T['types'],
581
+ U extends keyof T['types'][K],
582
+ W extends keyof T['types'][K],
583
+ X extends keyof T['types'][K],
584
+ >(params: CreateObjectsOptions<T, K, U, W, X>): Promise<Array<{ id: string }>> {
585
+ const { className, data } = params
586
+
587
+ const client = await this.pool.connect()
588
+
589
+ try {
590
+ if (data.length === 0) return []
591
+
592
+ // Every line is empty
593
+ if (data.every((item) => Object.keys(item).length === 0)) {
594
+ const placeholders = data.map(() => 'DEFAULT VALUES').join(', ')
595
+
596
+ const result = await client.query(
597
+ `INSERT INTO "${String(className)}" ${placeholders} RETURNING _id`,
598
+ )
599
+
600
+ return result.rows.map((row) => ({ id: row._id }))
601
+ }
602
+
603
+ const allColumns = Array.from(new Set(data.flatMap((item) => Object.keys(item))))
604
+
605
+ const columns = allColumns.map((column) => `"${column}"`)
606
+
607
+ const values = data.flatMap((item: any) =>
608
+ allColumns.map((col) => {
609
+ const value = item[col]
610
+ return Array.isArray(value) ? JSON.stringify(value) : (value ?? null)
611
+ }),
612
+ )
613
+
614
+ const placeholders = data.map((_, rowIndex) => {
615
+ const offset = rowIndex * allColumns.length
616
+ return `(${allColumns.map((_, colIndex) => `$${offset + colIndex + 1}`).join(', ')})`
617
+ })
618
+
619
+ const result = await client.query(
620
+ `INSERT INTO "${String(className)}" (${columns.join(
621
+ ', ',
622
+ )}) VALUES ${placeholders.join(', ')} RETURNING _id`,
623
+ values,
624
+ )
625
+
626
+ return result.rows.map((row) => ({ id: row._id }))
627
+ } finally {
628
+ client.release()
629
+ }
630
+ }
631
+
632
+ async updateObject<
633
+ K extends keyof T['types'],
634
+ U extends keyof T['types'][K],
635
+ W extends keyof T['types'][K],
636
+ >(params: UpdateObjectOptions<T, K, U, W>): Promise<{ id: string }> {
637
+ const { className, id, data, where } = params
638
+
639
+ const client = await this.pool.connect()
640
+
641
+ try {
642
+ const dataKeys = Object.keys(data)
643
+ const { query, values } = buildPostgresWhereQueryAndValues(where, dataKeys.length + 2)
644
+
645
+ const whereClause = query ? `AND ${query}` : ''
646
+
647
+ const setClause = dataKeys.map((key, index) => `"${key}" = $${index + 2}`).join(', ')
648
+
649
+ const result = await client.query(
650
+ `UPDATE "${String(className)}" SET ${setClause} WHERE _id = $1 ${whereClause} RETURNING _id`,
651
+ [id, ...computeValuesFromData(data), ...values],
652
+ )
653
+
654
+ if (result.rowCount === 0) throw new Error('Object not found')
655
+
656
+ return { id }
657
+ } finally {
658
+ client.release()
659
+ }
660
+ }
661
+
662
+ async updateObjects<
663
+ K extends keyof T['types'],
664
+ U extends keyof T['types'][K],
665
+ W extends keyof T['types'][K],
666
+ X extends keyof T['types'][K],
667
+ >(params: UpdateObjectsOptions<T, K, U, W, X>): Promise<Array<{ id: string }>> {
668
+ const { className, where, data, offset, first, context, order } = params
669
+
670
+ const client = await this.pool.connect()
671
+
672
+ try {
673
+ const objectsBeforeUpdate = await context.wabe.controllers.database.getObjects({
674
+ className,
675
+ where,
676
+ // @ts-expect-error
677
+ select: { id: true },
678
+ offset,
679
+ first,
680
+ context: {
681
+ ...context,
682
+ isRoot: true,
683
+ },
684
+ order,
685
+ })
686
+
687
+ if (objectsBeforeUpdate.length === 0) return []
688
+
689
+ const objectIds = objectsBeforeUpdate.filter(Boolean).map((obj: any) => obj.id) as string[]
690
+
691
+ await Promise.all(
692
+ objectIds.map(async (id) => {
693
+ const setClause = Object.keys(data)
694
+ .map((key, index) => `"${key}" = $${index + 1}`)
695
+ .join(', ')
696
+
697
+ await client.query(
698
+ `UPDATE "${String(className)}" SET ${setClause} WHERE _id = $${Object.keys(data).length + 1}`,
699
+ [...computeValuesFromData(data), id],
700
+ )
701
+ }),
702
+ )
703
+
704
+ return objectsBeforeUpdate.filter(Boolean).map((obj: any) => ({ id: obj.id }))
705
+ } finally {
706
+ client.release()
707
+ }
708
+ }
709
+
710
+ async deleteObject<K extends keyof T['types'], U extends keyof T['types'][K]>(
711
+ params: DeleteObjectOptions<T, K, U>,
712
+ ): Promise<void> {
713
+ const { className, id, where } = params
714
+
715
+ const client = await this.pool.connect()
716
+
717
+ try {
718
+ const { query, values } = buildPostgresWhereQueryAndValues(where, 2)
719
+
720
+ const whereClause = query ? `AND ${query}` : ''
721
+
722
+ const result = await client.query(
723
+ `DELETE FROM "${String(className)}" WHERE _id = $1 ${whereClause}`,
724
+ [id, ...values],
725
+ )
726
+
727
+ if (result.rowCount === 0) throw new Error('Object not found')
728
+ } finally {
729
+ client.release()
730
+ }
731
+ }
732
+
733
+ async deleteObjects<
734
+ K extends keyof T['types'],
735
+ U extends keyof T['types'][K],
736
+ W extends keyof T['types'][K],
737
+ >(params: DeleteObjectsOptions<T, K, U, W>): Promise<void> {
738
+ const { className, where } = params
739
+
740
+ const client = await this.pool.connect()
741
+
742
+ try {
743
+ const { query, values } = buildPostgresWhereQueryAndValues(where)
744
+
745
+ const whereClause = query ? `WHERE ${query}` : ''
746
+
747
+ await client.query(`DELETE FROM "${String(className)}" ${whereClause}`, values)
748
+ } finally {
749
+ client.release()
750
+ }
751
+ }
752
+ }