-
Notifications
You must be signed in to change notification settings - Fork 7
/
query-builder.ts
116 lines (105 loc) · 3.35 KB
/
query-builder.ts
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
import { TaggedTypeFormat } from "./tagged-type";
import type {
FQLFragment,
QueryValue,
QueryInterpolation,
} from "./wire-protocol";
/**
* A QueryArgumentObject is a plain javascript object where each property is a
* valid QueryArgument.
* These objects can be set as values in the {@link fql} function.
*/
export type QueryArgumentObject = {
[key: string]: QueryArgument;
};
/**
* A QueryArgument is a plain javascript object where each property is a
* valid QueryArgument.
* These objects can be set as values in the {@link fql} function.
*/
export type QueryArgument =
| QueryValue
| Query
| Date
| ArrayBuffer
| Uint8Array
| Array<QueryArgument>
| QueryArgumentObject;
/**
* Creates a new Query. Accepts template literal inputs.
* @param queryFragments - a {@link TemplateStringsArray} that constitute
* the strings that are the basis of the query.
* @param queryArgs - an Array\<QueryValue | Query\> that
* constitute the arguments to inject between the queryFragments.
* @throws Error - if you call this method directly (not using template
* literals) and pass invalid construction parameters
* @example
* ```typescript
* const str = "baz";
* const num = 17;
* const innerQuery = fql`${num} + 3)`;
* const queryRequestBuilder = fql`${str}.length == ${innerQuery}`;
* ```
*/
export function fql(
queryFragments: ReadonlyArray<string>,
...queryArgs: QueryArgument[]
): Query {
return new Query(queryFragments, ...queryArgs);
}
/**
* Internal class.
* A builder for composing queries using the {@link fql} tagged template
* function
*/
export class Query {
readonly #queryFragments: ReadonlyArray<string>;
readonly #interpolatedArgs: QueryArgument[];
constructor(
queryFragments: ReadonlyArray<string>,
...queryArgs: QueryArgument[]
) {
if (
queryFragments.length === 0 ||
queryFragments.length !== queryArgs.length + 1
) {
throw new Error("invalid query constructed");
}
this.#queryFragments = queryFragments;
this.#interpolatedArgs = queryArgs;
}
/**
* Converts this Query to an {@link FQLFragment} you can send
* to Fauna.
* @returns a {@link FQLFragment}.
* @example
* ```typescript
* const num = 8;
* const queryBuilder = fql`'foo'.length == ${num}`;
* const queryRequest = queryBuilder.toQuery();
* // produces:
* { fql: ["'foo'.length == ", { value: { "@int": "8" } }, ""] }
* ```
*/
encode(): FQLFragment {
if (this.#queryFragments.length === 1) {
return { fql: [this.#queryFragments[0]] };
}
let renderedFragments: (string | QueryInterpolation)[] =
this.#queryFragments.flatMap((fragment, i) => {
// There will always be one more fragment than there are arguments
if (i === this.#queryFragments.length - 1) {
return fragment === "" ? [] : [fragment];
}
// arguments in the template format must always be encoded, regardless
// of the "x-format" request header
// TODO: catch and rethrow Errors, indicating bad user input
const arg = this.#interpolatedArgs[i];
const encoded = TaggedTypeFormat.encodeInterpolation(arg);
return [fragment, encoded];
});
// We don't need to send empty-string fragments over the wire
renderedFragments = renderedFragments.filter((x) => x !== "");
return { fql: renderedFragments };
}
}