Skip to content

Commit 74c8bb6

Browse files
committed
wip
1 parent e1d9501 commit 74c8bb6

File tree

11 files changed

+397
-24
lines changed

11 files changed

+397
-24
lines changed

src/PostgrestFilterBuilder.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import PostgrestTransformBuilder from './PostgrestTransformBuilder'
22
import { JsonPathToAccessor, JsonPathToType } from './select-query-parser/utils'
33
import { GenericSchema } from './types'
4+
import { HeaderManager } from './utils'
45

56
type FilterOperator =
67
| 'eq'
@@ -69,13 +70,29 @@ type ResolveFilterRelationshipValue<
6970
: unknown
7071
: never
7172

73+
export type InvalidMethodError<S extends string> = { Error: S }
74+
7275
export default class PostgrestFilterBuilder<
7376
Schema extends GenericSchema,
7477
Row extends Record<string, unknown>,
7578
Result,
7679
RelationName = unknown,
77-
Relationships = unknown
78-
> extends PostgrestTransformBuilder<Schema, Row, Result, RelationName, Relationships> {
80+
Relationships = unknown,
81+
Method = unknown
82+
> extends PostgrestTransformBuilder<Schema, Row, Result, RelationName, Relationships, Method> {
83+
maxAffected(
84+
value: number
85+
): Method extends 'PATCH' | 'DELETE'
86+
? this
87+
: InvalidMethodError<'maxAffected method only available on update or delete'> {
88+
const preferHeaderManager = new HeaderManager('Prefer', this.headers['Prefer'])
89+
preferHeaderManager.add('handling=strict')
90+
preferHeaderManager.add(`max-affected=${value}`)
91+
this.headers['Prefer'] = preferHeaderManager.get()
92+
return this as unknown as Method extends 'PATCH' | 'DELETE'
93+
? this
94+
: InvalidMethodError<'maxAffected method only available on update or delete'>
95+
}
7996
/**
8097
* Match only rows where `column` is equal to `value`.
8198
*

src/PostgrestQueryBuilder.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,14 @@ export default class PostgrestQueryBuilder<
6666
head?: boolean
6767
count?: 'exact' | 'planned' | 'estimated'
6868
} = {}
69-
): PostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], RelationName, Relationships> {
69+
): PostgrestFilterBuilder<
70+
Schema,
71+
Relation['Row'],
72+
ResultOne[],
73+
RelationName,
74+
Relationships,
75+
'GET'
76+
> {
7077
const method = head ? 'HEAD' : 'GET'
7178
// Remove whitespaces except when quoted
7279
let quoted = false
@@ -103,14 +110,14 @@ export default class PostgrestQueryBuilder<
103110
options?: {
104111
count?: 'exact' | 'planned' | 'estimated'
105112
}
106-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
113+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships, 'POST'>
107114
insert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
108115
values: Row[],
109116
options?: {
110117
count?: 'exact' | 'planned' | 'estimated'
111118
defaultToNull?: boolean
112119
}
113-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
120+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships, 'POST'>
114121
/**
115122
* Perform an INSERT into the table or view.
116123
*
@@ -146,7 +153,7 @@ export default class PostgrestQueryBuilder<
146153
count?: 'exact' | 'planned' | 'estimated'
147154
defaultToNull?: boolean
148155
} = {}
149-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
156+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships, 'POST'> {
150157
const method = 'POST'
151158

152159
const prefersHeaders = []
@@ -188,7 +195,7 @@ export default class PostgrestQueryBuilder<
188195
ignoreDuplicates?: boolean
189196
count?: 'exact' | 'planned' | 'estimated'
190197
}
191-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
198+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships, 'POST'>
192199
upsert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
193200
values: Row[],
194201
options?: {
@@ -197,7 +204,7 @@ export default class PostgrestQueryBuilder<
197204
count?: 'exact' | 'planned' | 'estimated'
198205
defaultToNull?: boolean
199206
}
200-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships>
207+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships, 'POST'>
201208
/**
202209
* Perform an UPSERT on the table or view. Depending on the column(s) passed
203210
* to `onConflict`, `.upsert()` allows you to perform the equivalent of
@@ -249,7 +256,7 @@ export default class PostgrestQueryBuilder<
249256
count?: 'exact' | 'planned' | 'estimated'
250257
defaultToNull?: boolean
251258
} = {}
252-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
259+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships, 'POST'> {
253260
const method = 'POST'
254261

255262
const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]
@@ -313,7 +320,7 @@ export default class PostgrestQueryBuilder<
313320
}: {
314321
count?: 'exact' | 'planned' | 'estimated'
315322
} = {}
316-
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
323+
): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships, 'PATCH'> {
317324
const method = 'PATCH'
318325
const prefersHeaders = []
319326
if (this.headers['Prefer']) {
@@ -358,7 +365,14 @@ export default class PostgrestQueryBuilder<
358365
count,
359366
}: {
360367
count?: 'exact' | 'planned' | 'estimated'
361-
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], null, RelationName, Relationships> {
368+
} = {}): PostgrestFilterBuilder<
369+
Schema,
370+
Relation['Row'],
371+
null,
372+
RelationName,
373+
Relationships,
374+
'DELETE'
375+
> {
362376
const method = 'DELETE'
363377
const prefersHeaders = []
364378
if (count) {

src/PostgrestTransformBuilder.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export default class PostgrestTransformBuilder<
77
Row extends Record<string, unknown>,
88
Result,
99
RelationName = unknown,
10-
Relationships = unknown
10+
Relationships = unknown,
11+
Method = unknown
1112
> extends PostgrestBuilder<Result> {
1213
/**
1314
* Perform a SELECT on the query result.
@@ -23,7 +24,7 @@ export default class PostgrestTransformBuilder<
2324
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query>
2425
>(
2526
columns?: Query
26-
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], RelationName, Relationships> {
27+
): PostgrestTransformBuilder<Schema, Row, NewResultOne[], RelationName, Relationships, Method> {
2728
// Remove whitespaces except when quoted
2829
let quoted = false
2930
const cleanedColumns = (columns ?? '*')
@@ -48,7 +49,8 @@ export default class PostgrestTransformBuilder<
4849
Row,
4950
NewResultOne[],
5051
RelationName,
51-
Relationships
52+
Relationships,
53+
Method
5254
>
5355
}
5456

@@ -314,14 +316,16 @@ export default class PostgrestTransformBuilder<
314316
Row,
315317
CheckMatchingArrayTypes<Result, NewResult>,
316318
RelationName,
317-
Relationships
319+
Relationships,
320+
Method
318321
> {
319322
return this as unknown as PostgrestTransformBuilder<
320323
Schema,
321324
Row,
322325
CheckMatchingArrayTypes<Result, NewResult>,
323326
RelationName,
324-
Relationships
327+
Relationships,
328+
Method
325329
>
326330
}
327331
}

src/select-query-parser/result.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,26 @@ type ProcessSpreadNode<
401401
? Result extends SelectQueryError<infer E>
402402
? SelectQueryError<E>
403403
: ExtractFirstProperty<Result> extends unknown[]
404-
? {
405-
[K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`>
406-
}
404+
? // Spread over an many-to-many relationship, turn all the result fields into arrays
405+
ProcessManyToManySpreadNodeResult<Result>
407406
: ProcessSpreadNodeResult<Result>
408407
: never
409408

409+
/**
410+
* Helper type to process the result of a many-to-many spread node.
411+
* Converts all fields in the spread object into arrays.
412+
*/
413+
type ProcessManyToManySpreadNodeResult<Result> = Result extends Record<
414+
string,
415+
SelectQueryError<string> | null
416+
>
417+
? Result
418+
: ExtractFirstProperty<Result> extends infer SpreadedObject
419+
? SpreadedObject extends Array<Record<string, unknown>>
420+
? { [K in keyof SpreadedObject[number]]: Array<SpreadedObject[number][K]> }
421+
: SelectQueryError<'An error occurred spreading the many-to-many object'>
422+
: SelectQueryError<'An error occurred spreading the many-to-many object'>
423+
410424
/**
411425
* Helper type to process the result of a spread node.
412426
*/

src/utils.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
export class HeaderManager {
2+
private headers: Map<string, Set<string>> = new Map()
3+
4+
/**
5+
* Create a new HeaderManager, optionally parsing an existing header string
6+
* @param header The header name to manage
7+
* @param existingValue Optional existing header value to parse
8+
*/
9+
constructor(private readonly header: string, existingValue?: string) {
10+
if (existingValue) {
11+
this.parseHeaderString(existingValue)
12+
}
13+
}
14+
15+
/**
16+
* Parse an existing header string into the internal Set
17+
* @param headerString The header string to parse
18+
*/
19+
private parseHeaderString(headerString: string): void {
20+
if (!headerString.trim()) return
21+
22+
const values = headerString.split(',')
23+
values.forEach((value) => {
24+
const trimmedValue = value.trim()
25+
if (trimmedValue) {
26+
this.add(trimmedValue)
27+
}
28+
})
29+
}
30+
31+
/**
32+
* Add a value to the header. If the header doesn't exist, it will be created.
33+
* @param value The value to add
34+
*/
35+
add(value: string): void {
36+
if (!this.headers.has(this.header)) {
37+
this.headers.set(this.header, new Set())
38+
}
39+
this.headers.get(this.header)!.add(value)
40+
}
41+
42+
/**
43+
* Get the formatted string value for the header
44+
*/
45+
get(): string {
46+
const values = this.headers.get(this.header)
47+
return values ? Array.from(values).join(',') : ''
48+
}
49+
50+
/**
51+
* Check if the header has a specific value
52+
* @param value The value to check
53+
*/
54+
has(value: string): boolean {
55+
return this.headers.get(this.header)?.has(value) ?? false
56+
}
57+
58+
/**
59+
* Clear all values for the header
60+
*/
61+
clear(): void {
62+
this.headers.delete(this.header)
63+
}
64+
}

test/db/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
version: '3'
44
services:
55
rest:
6-
image: postgrest/postgrest:v12.2.0
6+
image: postgrest/postgrest:v13.0.0
77
ports:
88
- '3000:3000'
99
environment:

test/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ import './filters'
44
import './resource-embedding'
55
import './transforms'
66
import './rpc'
7+
import './utils'
8+
// import './max-affected'

0 commit comments

Comments
 (0)