-
-
Notifications
You must be signed in to change notification settings - Fork 19
/
index.ts
166 lines (142 loc) · 3.77 KB
/
index.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
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
/**
* Values supported by SQL engine.
*/
export type Value = unknown;
/**
* Supported value or SQL instance.
*/
export type RawValue = Value | Sql;
/**
* A SQL instance can be nested within each other to build SQL strings.
*/
export class Sql {
readonly values: Value[];
readonly strings: string[];
constructor(rawStrings: readonly string[], rawValues: readonly RawValue[]) {
if (rawStrings.length - 1 !== rawValues.length) {
if (rawStrings.length === 0) {
throw new TypeError("Expected at least 1 string");
}
throw new TypeError(
`Expected ${rawStrings.length} strings to have ${
rawStrings.length - 1
} values`
);
}
const valuesLength = rawValues.reduce<number>(
(len, value) => len + (value instanceof Sql ? value.values.length : 1),
0
);
this.values = new Array(valuesLength);
this.strings = new Array(valuesLength + 1);
this.strings[0] = rawStrings[0];
// Iterate over raw values, strings, and children. The value is always
// positioned between two strings, e.g. `index + 1`.
let i = 0,
pos = 0;
while (i < rawValues.length) {
const child = rawValues[i++];
const rawString = rawStrings[i];
// Check for nested `sql` queries.
if (child instanceof Sql) {
// Append child prefix text to current string.
this.strings[pos] += child.strings[0];
let childIndex = 0;
while (childIndex < child.values.length) {
this.values[pos++] = child.values[childIndex++];
this.strings[pos] = child.strings[childIndex];
}
// Append raw string to current string.
this.strings[pos] += rawString;
} else {
this.values[pos++] = child;
this.strings[pos] = rawString;
}
}
}
get text() {
let i = 1,
value = this.strings[0];
while (i < this.strings.length) value += `$${i}${this.strings[i++]}`;
return value;
}
get sql() {
let i = 1,
value = this.strings[0];
while (i < this.strings.length) value += `?${this.strings[i++]}`;
return value;
}
inspect() {
return {
text: this.text,
sql: this.sql,
values: this.values,
};
}
}
/**
* Create a SQL query for a list of values.
*/
export function join(
values: readonly RawValue[],
separator = ",",
prefix = "",
suffix = ""
) {
if (values.length === 0) {
throw new TypeError(
"Expected `join([])` to be called with an array of multiple elements, but got an empty array"
);
}
return new Sql(
[prefix, ...Array(values.length - 1).fill(separator), suffix],
values
);
}
/**
* Create a SQL query for a list of structured values.
*/
export function bulk(
data: ReadonlyArray<ReadonlyArray<RawValue>>,
separator = ",",
prefix = "",
suffix = ""
) {
const length = data.length && data[0].length;
if (length === 0) {
throw new TypeError(
"Expected `bulk([][])` to be called with a nested array of multiple elements, but got an empty array"
);
}
const values = data.map((item, index) => {
if (item.length !== length) {
throw new TypeError(
`Expected \`bulk([${index}][])\` to have a length of ${length}, but got ${item.length}`
);
}
return new Sql(["(", ...Array(item.length - 1).fill(separator), ")"], item);
});
return new Sql(
[prefix, ...Array(values.length - 1).fill(separator), suffix],
values
);
}
/**
* Create raw SQL statement.
*/
export function raw(value: string) {
return new Sql([value], []);
}
/**
* Placeholder value for "no text".
*/
export const empty = raw("");
/**
* Create a SQL object from a template string.
*/
export default function sql(
strings: readonly string[],
...values: readonly RawValue[]
) {
return new Sql(strings, values);
}