@tymber/common 0.0.1-alpha.0 → 0.1.0
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/LICENSE +22 -0
- package/README.md +46 -0
- package/dist/App.d.ts +17 -0
- package/dist/App.js +236 -0
- package/dist/Component.d.ts +16 -0
- package/dist/Component.js +116 -0
- package/dist/ConfigService.d.ts +31 -0
- package/dist/ConfigService.js +75 -0
- package/dist/Context.d.ts +41 -0
- package/dist/Context.js +10 -0
- package/dist/DB.d.ts +15 -0
- package/dist/DB.js +7 -0
- package/dist/Endpoint.d.ts +21 -0
- package/dist/Endpoint.js +87 -0
- package/dist/EventEmitter.d.ts +9 -0
- package/dist/EventEmitter.js +21 -0
- package/dist/Handler.d.ts +8 -0
- package/dist/Handler.js +8 -0
- package/dist/HttpContext.d.ts +26 -0
- package/dist/HttpContext.js +11 -0
- package/dist/I18nService.d.ts +18 -0
- package/dist/I18nService.js +72 -0
- package/dist/Middleware.d.ts +6 -0
- package/dist/Middleware.js +4 -0
- package/dist/Module.d.ts +47 -0
- package/dist/Module.js +12 -0
- package/dist/PubSubService.d.ts +20 -0
- package/dist/PubSubService.js +60 -0
- package/dist/Repository.d.ts +29 -0
- package/dist/Repository.js +110 -0
- package/dist/Router.d.ts +10 -0
- package/dist/Router.js +53 -0
- package/dist/TemplateService.d.ts +22 -0
- package/dist/TemplateService.js +66 -0
- package/dist/View.d.ts +17 -0
- package/dist/View.js +48 -0
- package/dist/ViewRenderer.d.ts +16 -0
- package/dist/ViewRenderer.js +59 -0
- package/dist/contrib/accept-language-parser.d.ts +9 -0
- package/dist/contrib/accept-language-parser.js +73 -0
- package/dist/contrib/cookie.d.ts +33 -0
- package/dist/contrib/cookie.js +207 -0
- package/dist/contrib/template.d.ts +1 -0
- package/dist/contrib/template.js +107 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +32 -0
- package/dist/utils/ajv.d.ts +2 -0
- package/dist/utils/ajv.js +10 -0
- package/dist/utils/camelToSnakeCase.d.ts +1 -0
- package/dist/utils/camelToSnakeCase.js +3 -0
- package/dist/utils/computeBaseUrl.d.ts +1 -0
- package/dist/utils/computeBaseUrl.js +37 -0
- package/dist/utils/computeContentType.d.ts +1 -0
- package/dist/utils/computeContentType.js +17 -0
- package/dist/utils/createDebug.d.ts +1 -0
- package/dist/utils/createDebug.js +4 -0
- package/dist/utils/createTestApp.d.ts +8 -0
- package/dist/utils/createTestApp.js +57 -0
- package/dist/utils/escapeValue.d.ts +1 -0
- package/dist/utils/escapeValue.js +3 -0
- package/dist/utils/fs.d.ts +6 -0
- package/dist/utils/fs.js +13 -0
- package/dist/utils/isAdmin.d.ts +2 -0
- package/dist/utils/isAdmin.js +4 -0
- package/dist/utils/isProduction.d.ts +1 -0
- package/dist/utils/isProduction.js +1 -0
- package/dist/utils/loadModules.d.ts +3 -0
- package/dist/utils/loadModules.js +83 -0
- package/dist/utils/randomId.d.ts +1 -0
- package/dist/utils/randomId.js +4 -0
- package/dist/utils/randomUUID.d.ts +1 -0
- package/dist/utils/randomUUID.js +4 -0
- package/dist/utils/runMigrations.d.ts +3 -0
- package/dist/utils/runMigrations.js +85 -0
- package/dist/utils/snakeToCamelCase.d.ts +1 -0
- package/dist/utils/snakeToCamelCase.js +3 -0
- package/dist/utils/sortBy.d.ts +1 -0
- package/dist/utils/sortBy.js +8 -0
- package/dist/utils/sql.d.ts +120 -0
- package/dist/utils/sql.js +433 -0
- package/dist/utils/toNodeHandler.d.ts +2 -0
- package/dist/utils/toNodeHandler.js +91 -0
- package/dist/utils/types.d.ts +3 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/waitFor.d.ts +1 -0
- package/dist/utils/waitFor.js +5 -0
- package/package.json +33 -2
- package/src/App.ts +319 -0
- package/src/Component.ts +166 -0
- package/src/ConfigService.ts +121 -0
- package/src/Context.ts +60 -0
- package/src/DB.ts +28 -0
- package/src/Endpoint.ts +118 -0
- package/src/EventEmitter.ts +32 -0
- package/src/Handler.ts +14 -0
- package/src/HttpContext.ts +35 -0
- package/src/I18nService.ts +96 -0
- package/src/Middleware.ts +10 -0
- package/src/Module.ts +60 -0
- package/src/PubSubService.ts +77 -0
- package/src/Repository.ts +158 -0
- package/src/Router.ts +77 -0
- package/src/TemplateService.ts +97 -0
- package/src/View.ts +60 -0
- package/src/ViewRenderer.ts +71 -0
- package/src/contrib/accept-language-parser.ts +94 -0
- package/src/contrib/cookie.ts +256 -0
- package/src/contrib/template.ts +134 -0
- package/src/index.ts +54 -0
- package/src/utils/ajv.ts +13 -0
- package/src/utils/camelToSnakeCase.ts +3 -0
- package/src/utils/computeBaseUrl.ts +46 -0
- package/src/utils/computeContentType.ts +17 -0
- package/src/utils/createDebug.ts +5 -0
- package/src/utils/createTestApp.ts +84 -0
- package/src/utils/escapeValue.ts +3 -0
- package/src/utils/fs.ts +15 -0
- package/src/utils/isAdmin.ts +5 -0
- package/src/utils/isProduction.ts +2 -0
- package/src/utils/loadModules.ts +105 -0
- package/src/utils/randomId.ts +5 -0
- package/src/utils/randomUUID.ts +5 -0
- package/src/utils/runMigrations.ts +122 -0
- package/src/utils/snakeToCamelCase.ts +3 -0
- package/src/utils/sortBy.ts +8 -0
- package/src/utils/sql.ts +553 -0
- package/src/utils/toNodeHandler.ts +121 -0
- package/src/utils/types.ts +1 -0
- package/src/utils/waitFor.ts +5 -0
package/src/utils/sql.ts
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
export function sql() {}
|
|
2
|
+
|
|
3
|
+
interface Options {
|
|
4
|
+
placeholder: string;
|
|
5
|
+
quoteChar: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options: Options = {
|
|
9
|
+
placeholder: "$%d",
|
|
10
|
+
quoteChar: '"',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
sql.setOption = function <T extends keyof Options>(o: T, value: Options[T]) {
|
|
14
|
+
options[o] = value;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface BuildContext {
|
|
18
|
+
values: any[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function joinParts(parts: Array<string | undefined>) {
|
|
22
|
+
return parts.filter((s) => !!s).join(" ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export abstract class Statement {
|
|
26
|
+
build() {
|
|
27
|
+
const values: any[] = [];
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
text: joinParts(
|
|
31
|
+
this.computeParts({
|
|
32
|
+
values,
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
values,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected abstract computeParts(ctx: BuildContext): Array<string | undefined>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Expression = (ctx: BuildContext) => string;
|
|
43
|
+
|
|
44
|
+
function joinExpression(type: string, table: string, on: Record<string, any>) {
|
|
45
|
+
return () => {
|
|
46
|
+
// we cannot use sql.and() since the right part of the expression must be escaped with handleColumn() instead of handleValue()
|
|
47
|
+
const condition = Object.entries(on)
|
|
48
|
+
.map(([column, value]) => {
|
|
49
|
+
return handleColumn(column) + " = " + handleColumn(value);
|
|
50
|
+
})
|
|
51
|
+
.join(" AND ");
|
|
52
|
+
|
|
53
|
+
return `${type} JOIN ${handleTable(table)} ON ${condition}`;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class SelectStatement extends Statement {
|
|
58
|
+
private _table?: string;
|
|
59
|
+
private _distinct = false;
|
|
60
|
+
private _columns: Array<string | Expression> = [];
|
|
61
|
+
private _joins: Expression[] = [];
|
|
62
|
+
private _where: Expression[] = [];
|
|
63
|
+
private _orderBy: string[] = [];
|
|
64
|
+
private _groupBy: string[] = [];
|
|
65
|
+
private _having?: Expression;
|
|
66
|
+
private _limit?: number;
|
|
67
|
+
private _offset?: number;
|
|
68
|
+
private _forUpdate = false;
|
|
69
|
+
|
|
70
|
+
constructor(columns?: Array<string | Expression>) {
|
|
71
|
+
super();
|
|
72
|
+
if (columns) {
|
|
73
|
+
this._columns.push(...columns);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
distinct() {
|
|
78
|
+
this._distinct = true;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
from(table: string) {
|
|
83
|
+
this._table = table;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
innerJoin(table: string, on: Record<string, any>) {
|
|
88
|
+
this._joins.push(joinExpression("INNER", table, on));
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
leftJoin(table: string, on: Record<string, any>) {
|
|
93
|
+
this._joins.push(joinExpression("LEFT", table, on));
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
rightJoin(table: string, on: Record<string, any>) {
|
|
98
|
+
this._joins.push(joinExpression("RIGHT", table, on));
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fullOuterJoin(table: string, on: Record<string, any>) {
|
|
103
|
+
this._joins.push(joinExpression("FULL OUTER", table, on));
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
where(arg: Record<string, any> | Expression) {
|
|
108
|
+
if (typeof arg === "function") {
|
|
109
|
+
this._where.push(arg as Expression);
|
|
110
|
+
} else {
|
|
111
|
+
for (const [column, value] of Object.entries(arg)) {
|
|
112
|
+
this._where.push(sql.eq(column, value));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
groupBy(columns: string[]) {
|
|
119
|
+
this._groupBy.push(...columns);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
having(expr: Expression) {
|
|
124
|
+
this._having = expr;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
orderBy(columns: string[]) {
|
|
129
|
+
this._orderBy.push(...columns);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
limit(limit: number) {
|
|
134
|
+
this._limit = limit;
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
offset(offset: number) {
|
|
139
|
+
this._offset = offset;
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
forUpdate() {
|
|
144
|
+
this._forUpdate = true;
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected override computeParts(ctx: BuildContext) {
|
|
149
|
+
return [
|
|
150
|
+
"SELECT",
|
|
151
|
+
this.distinctPart(),
|
|
152
|
+
this.columnsPart(ctx),
|
|
153
|
+
this.fromPart(),
|
|
154
|
+
this.joinsPart(ctx),
|
|
155
|
+
this.wherePart(ctx),
|
|
156
|
+
this.groupByPart(),
|
|
157
|
+
this.havingPart(ctx),
|
|
158
|
+
this.orderByPart(),
|
|
159
|
+
this.limitPart(ctx),
|
|
160
|
+
this.offsetPart(ctx),
|
|
161
|
+
this.forUpdatePart(),
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected distinctPart() {
|
|
166
|
+
if (this._distinct) {
|
|
167
|
+
return "DISTINCT";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected columnsPart(ctx: BuildContext) {
|
|
172
|
+
if (this._columns.length) {
|
|
173
|
+
return this._columns
|
|
174
|
+
.map((column) => {
|
|
175
|
+
return typeof column === "function"
|
|
176
|
+
? column(ctx)
|
|
177
|
+
: handleColumn(column);
|
|
178
|
+
})
|
|
179
|
+
.join(", ");
|
|
180
|
+
} else {
|
|
181
|
+
return "*";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
protected fromPart() {
|
|
186
|
+
if (this._table) {
|
|
187
|
+
return `FROM ${handleTable(this._table)}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
protected joinsPart(ctx: BuildContext) {
|
|
192
|
+
if (this._joins.length) {
|
|
193
|
+
return this._joins.map((join) => join(ctx)).join(" ");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
protected wherePart(ctx: BuildContext) {
|
|
198
|
+
if (this._where.length) {
|
|
199
|
+
return "WHERE " + groupExpression(this._where, " AND ", false)(ctx);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected groupByPart() {
|
|
204
|
+
if (this._groupBy.length) {
|
|
205
|
+
return "GROUP BY " + handleColumns(this._groupBy);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
protected havingPart(ctx: BuildContext) {
|
|
210
|
+
if (this._having) {
|
|
211
|
+
return "HAVING " + this._having(ctx);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
protected orderByPart() {
|
|
216
|
+
if (this._orderBy.length) {
|
|
217
|
+
return "ORDER BY " + handleColumns(this._orderBy);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected limitPart(ctx: BuildContext) {
|
|
222
|
+
if (this._limit) {
|
|
223
|
+
// PostgreSQL/SQLite syntax
|
|
224
|
+
return "LIMIT " + handleValue(this._limit, ctx);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
protected offsetPart(ctx: BuildContext) {
|
|
229
|
+
if (this._offset) {
|
|
230
|
+
// PostgreSQL/SQLite syntax
|
|
231
|
+
return "OFFSET " + handleValue(this._offset, ctx);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
protected forUpdatePart() {
|
|
236
|
+
if (this._forUpdate) {
|
|
237
|
+
return "FOR UPDATE";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
sql.select = (columns?: Array<string | Expression>) =>
|
|
243
|
+
new SelectStatement(columns);
|
|
244
|
+
|
|
245
|
+
class InsertStatement extends Statement {
|
|
246
|
+
private _table?: string;
|
|
247
|
+
private _values: Array<Record<string, any>> = [];
|
|
248
|
+
private _select?: SelectStatement;
|
|
249
|
+
private _returning: string[] = [];
|
|
250
|
+
|
|
251
|
+
into(table: string) {
|
|
252
|
+
this._table = table;
|
|
253
|
+
return this;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
values(values: Array<Record<string, any>>) {
|
|
257
|
+
this._values.push(...values);
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
select(statement: SelectStatement) {
|
|
262
|
+
this._select = statement;
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
returning(columns = ["*"]) {
|
|
267
|
+
this._returning.push(...columns);
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected override computeParts(ctx: BuildContext) {
|
|
272
|
+
return [
|
|
273
|
+
"INSERT",
|
|
274
|
+
this.intoPart(),
|
|
275
|
+
this.columnsPart(),
|
|
276
|
+
this.valuesPart(ctx),
|
|
277
|
+
this.returningPart(),
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
protected intoPart() {
|
|
282
|
+
if (this._table) {
|
|
283
|
+
return "INTO " + handleTable(this._table);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
protected columnsPart() {
|
|
288
|
+
if (this._values.length) {
|
|
289
|
+
const columns = Object.keys(this._values[0]!);
|
|
290
|
+
return "(" + columns.map(handleColumn).join(", ") + ")";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (this._select) {
|
|
294
|
+
// @ts-expect-error protected method
|
|
295
|
+
return "(" + this._select.columnsPart() + ")";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
protected valuesPart(ctx: BuildContext) {
|
|
300
|
+
if (this._values.length) {
|
|
301
|
+
const columns = Object.keys(this._values[0]!);
|
|
302
|
+
const values = this._values.map(
|
|
303
|
+
(values) =>
|
|
304
|
+
`(${columns.map((column) => handleValue(values[column], ctx)).join(", ")})`,
|
|
305
|
+
);
|
|
306
|
+
return "VALUES " + values.join(", ");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (this._select) {
|
|
310
|
+
// @ts-expect-error protected method
|
|
311
|
+
return joinParts(this._select.computeParts(ctx));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
protected returningPart() {
|
|
316
|
+
if (this._returning.length) {
|
|
317
|
+
return "RETURNING " + handleColumns(this._returning);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
sql.insert = () => new InsertStatement();
|
|
323
|
+
|
|
324
|
+
class UpdateStatement extends Statement {
|
|
325
|
+
private _values: Record<string, any> = {};
|
|
326
|
+
private _where: Expression[] = [];
|
|
327
|
+
|
|
328
|
+
constructor(private readonly table: string) {
|
|
329
|
+
super();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
set(values: Record<string, any>) {
|
|
333
|
+
Object.assign(this._values, values);
|
|
334
|
+
return this;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
where(arg: Record<string, any> | Expression) {
|
|
338
|
+
if (typeof arg === "function") {
|
|
339
|
+
this._where.push(arg as Expression);
|
|
340
|
+
} else {
|
|
341
|
+
for (const [column, value] of Object.entries(arg)) {
|
|
342
|
+
this._where.push(sql.eq(column, value));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
protected override computeParts(ctx: BuildContext) {
|
|
349
|
+
return [
|
|
350
|
+
`UPDATE ${handleTable(this.table)}`,
|
|
351
|
+
this.setPart(ctx),
|
|
352
|
+
this.wherePart(ctx),
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
protected setPart(ctx: BuildContext) {
|
|
357
|
+
const values = [];
|
|
358
|
+
|
|
359
|
+
for (const [column, value] of Object.entries(this._values)) {
|
|
360
|
+
values.push(`${handleColumn(column)} = ${handleValue(value, ctx)}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return "SET " + values.join(", ");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
protected wherePart(ctx: BuildContext) {
|
|
367
|
+
if (this._where.length) {
|
|
368
|
+
return "WHERE " + groupExpression(this._where, " AND ", false)(ctx);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
sql.update = (table: string) => new UpdateStatement(table);
|
|
374
|
+
|
|
375
|
+
class DeleteStatement extends Statement {
|
|
376
|
+
private readonly _table: string;
|
|
377
|
+
private _where: Expression[] = [];
|
|
378
|
+
|
|
379
|
+
constructor(table: string) {
|
|
380
|
+
super();
|
|
381
|
+
this._table = table;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
where(arg: Record<string, any> | Expression) {
|
|
385
|
+
if (typeof arg === "function") {
|
|
386
|
+
this._where.push(arg as Expression);
|
|
387
|
+
} else {
|
|
388
|
+
for (const [column, value] of Object.entries(arg)) {
|
|
389
|
+
this._where.push(sql.eq(column, value));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
protected override computeParts(ctx: BuildContext) {
|
|
396
|
+
return [`DELETE FROM ${handleTable(this._table)}`, this.wherePart(ctx)];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
protected wherePart(ctx: BuildContext) {
|
|
400
|
+
if (this._where.length) {
|
|
401
|
+
return "WHERE " + groupExpression(this._where, " AND ", false)(ctx);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
sql.deleteFrom = (table: string) => new DeleteStatement(table);
|
|
407
|
+
|
|
408
|
+
function groupExpression(
|
|
409
|
+
clauses: Expression[],
|
|
410
|
+
op: string,
|
|
411
|
+
includeParens = true,
|
|
412
|
+
) {
|
|
413
|
+
return (ctx: BuildContext) => {
|
|
414
|
+
const output = clauses.map((expr) => expr(ctx)).join(op);
|
|
415
|
+
return includeParens ? `(${output})` : output;
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
sql.and = (clauses: Expression[]) => groupExpression(clauses, " AND ");
|
|
420
|
+
sql.or = (clauses: Expression[]) => groupExpression(clauses, " OR ");
|
|
421
|
+
sql.not = (expr: Expression) => (ctx: BuildContext) => "NOT " + expr(ctx);
|
|
422
|
+
|
|
423
|
+
function unaryExpression(column: string, op: string) {
|
|
424
|
+
return () => `${handleColumn(column)} ${op}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
sql.isNull = (column: string) => unaryExpression(column, "IS NULL");
|
|
428
|
+
sql.isNotNull = (column: string) => unaryExpression(column, "IS NOT NULL");
|
|
429
|
+
|
|
430
|
+
function binaryExpression(column: string, op: string, value: any) {
|
|
431
|
+
return (ctx: BuildContext) =>
|
|
432
|
+
`${handleColumn(column)} ${op} ${handleValue(value, ctx)}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
sql.eq = (column: string, value: any) => {
|
|
436
|
+
return value === null
|
|
437
|
+
? sql.isNull(column)
|
|
438
|
+
: binaryExpression(column, "=", value);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
sql.notEq = (column: string, value: any) => {
|
|
442
|
+
return value === null
|
|
443
|
+
? sql.isNotNull(column)
|
|
444
|
+
: binaryExpression(column, "<>", value);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
sql.lt = (column: string, value: any) => binaryExpression(column, "<", value);
|
|
448
|
+
sql.lte = (column: string, value: any) => binaryExpression(column, "<=", value);
|
|
449
|
+
sql.gt = (column: string, value: any) => binaryExpression(column, ">", value);
|
|
450
|
+
sql.gte = (column: string, value: any) => binaryExpression(column, ">=", value);
|
|
451
|
+
|
|
452
|
+
sql.between = (column: string, low: any, high: any) => {
|
|
453
|
+
return (ctx: BuildContext) =>
|
|
454
|
+
`${handleColumn(column)} BETWEEN ${handleValue(low, ctx)} AND ${handleValue(high, ctx)}`;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
function likeExpression(
|
|
458
|
+
column: string,
|
|
459
|
+
op: string,
|
|
460
|
+
value: any,
|
|
461
|
+
escapeChar?: string,
|
|
462
|
+
) {
|
|
463
|
+
return (ctx: BuildContext) => {
|
|
464
|
+
const output = `${handleColumn(column)} ${op} ${handleValue(value, ctx)}`;
|
|
465
|
+
return escapeChar ? `${output} ESCAPE '${escapeChar}'` : output;
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
sql.like = (column: string, value: any, escapeChar?: string) =>
|
|
470
|
+
likeExpression(column, "LIKE", value, escapeChar);
|
|
471
|
+
|
|
472
|
+
sql.ilike = (column: string, value: any, escapeChar?: string) =>
|
|
473
|
+
likeExpression(column, "ILIKE", value, escapeChar);
|
|
474
|
+
|
|
475
|
+
sql.in = (column: string, values: any[]) => {
|
|
476
|
+
return (ctx: BuildContext) =>
|
|
477
|
+
`${handleColumn(column)} IN (${values.map((value) => handleValue(value, ctx)).join(", ")})`;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
sql.raw = (text: string, values: any[] = []) => {
|
|
481
|
+
return (ctx: BuildContext) => {
|
|
482
|
+
let i = 0;
|
|
483
|
+
return text.replace(/\?/g, () => {
|
|
484
|
+
if (i >= values.length) {
|
|
485
|
+
throw new Error("not enough values");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return handleValue(values[i++], ctx);
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
function handleColumns(columns: string[]) {
|
|
494
|
+
return columns.map(handleColumn).join(", ");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const WITH_SCHEMA_OR_ALIAS_REGEX = /^((\w+)\.)?(\w+)(( AS)? \w+)?$/i;
|
|
498
|
+
|
|
499
|
+
function handleTable(table: string) {
|
|
500
|
+
return handleColumn(table);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function handleColumn(name: string) {
|
|
504
|
+
const match = WITH_SCHEMA_OR_ALIAS_REGEX.exec(name);
|
|
505
|
+
|
|
506
|
+
if (match) {
|
|
507
|
+
const schema = match[2];
|
|
508
|
+
const table = match[3] as string;
|
|
509
|
+
const alias = match[4];
|
|
510
|
+
|
|
511
|
+
let output = schema
|
|
512
|
+
? `${quoteKey(schema)}.${quoteKey(table)}`
|
|
513
|
+
: quoteKey(table);
|
|
514
|
+
|
|
515
|
+
if (alias) {
|
|
516
|
+
output += alias;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return output;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return quoteKey(name);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const UPPERCASE_REGEX = /[A-Z]/;
|
|
526
|
+
|
|
527
|
+
// SQL:2023 specification: https://en.wikipedia.org/wiki/List_of_SQL_reserved_words
|
|
528
|
+
// prettier-ignore
|
|
529
|
+
const RESERVED_KEYWORDS = new Set(["abs", "absent", "acos", "all", "allocate", "alter", "and", "any", "any_value", "are", "array", "array_agg", "array_max_cardinality", "as", "asensitive", "asin", "asymmetric", "at", "atan", "atomic", "authorization", "avg", "begin", "begin_frame", "begin_partition", "between", "bigint", "binary", "blob", "boolean", "both", "btrim", "by", "call", "called", "cardinality", "cascaded", "case", "cast", "ceil", "ceiling", "char", "char_length", "character", "character_length", "check", "classifier", "clob", "close", "coalesce", "collate", "collect", "column", "commit", "condition", "connect", "constraint", "contains", "convert", "copy", "corr", "corresponding", "cos", "cosh", "count", "covar_pop", "covar_samp", "create", "cross", "cube", "cume_dist", "current", "current_catalog", "current_date", "current_default_transform_group", "current_path", "current_role", "current_row", "current_schema", "current_time", "current_timestamp", "current_transform_group_for_type", "current_user", "cursor", "cycle", "date", "day", "deallocate", "dec", "decfloat", "decimal", "declare", "default", "define", "delete", "dense_rank", "deref", "describe", "deterministic", "disconnect", "distinct", "double", "drop", "dynamic", "each", "element", "else", "empty", "end", "end_frame", "end_partition", "end-exec", "equals", "escape", "every", "except", "exec", "execute", "exists", "exp", "external", "extract", "false", "fetch", "filter", "first_value", "float", "floor", "for", "foreign", "frame_row", "free", "from", "full", "function", "fusion", "get", "global", "grant", "greatest", "group", "grouping", "groups", "having", "hold", "hour", "identity", "in", "indicator", "initial", "inner", "inout", "insensitive", "insert", "int", "integer", "intersect", "intersection", "interval", "into", "is", "join", "json", "json_array", "json_arrayagg", "json_exists", "json_object", "json_objectagg", "json_query", "json_scalar", "json_serialize", "json_table", "json_table_primitive", "json_value", "lag", "language", "large", "last_value", "lateral", "lead", "leading", "least", "left", "like", "like_regex", "listagg", "ln", "local", "localtime", "localtimestamp", "log", "log10", "lower", "lpad", "ltrim", "match", "match_number", "match_recognize", "matches", "max", "member", "merge", "method", "min", "minute", "mod", "modifies", "module", "month", "multiset", "national", "natural", "nchar", "nclob", "new", "no", "none", "normalize", "not", "nth_value", "ntile", "null", "nullif", "numeric", "occurrences_regex", "octet_length", "of", "offset", "old", "omit", "on", "one", "only", "open", "or", "order", "out", "outer", "over", "overlaps", "overlay", "parameter", "partition", "pattern", "per", "percent", "percent_rank", "percentile_cont", "percentile_disc", "period", "portion", "position", "position_regex", "power", "precedes", "precision", "prepare", "primary", "procedure", "ptf", "range", "rank", "reads", "real", "recursive", "ref", "references", "referencing", "regr_avgx", "regr_avgy", "regr_count", "regr_intercept", "regr_r2", "regr_slope", "regr_sxx", "regr_sxy", "regr_syy", "release", "result", "return", "returns", "revoke", "right", "rollback", "rollup", "row", "row_number", "rows", "rpad", "running", "savepoint", "scope", "scroll", "search", "second", "seek", "select", "sensitive", "session_user", "set", "show", "similar", "sin", "sinh", "skip", "smallint", "some", "specific", "specifictype", "sql", "sqlexception", "sqlstate", "sqlwarning", "sqrt", "start", "static", "stddev_pop", "stddev_samp", "submultiset", "subset", "substring", "substring_regex", "succeeds", "sum", "symmetric", "system", "system_time", "system_user", "table", "tablesample", "tan", "tanh", "then", "time", "timestamp", "timezone_hour", "timezone_minute", "to", "trailing", "translate", "translate_regex", "translation", "treat", "trigger", "trim", "trim_array", "true", "truncate", "uescape", "union", "unique", "unknown", "unnest", "update", "upper", "user", "using", "value", "value_of", "values", "var_pop", "var_samp", "varbinary", "varchar", "varying", "versioning", "when", "whenever", "where", "width_bucket", "window", "with", "within", "without", "year"]);
|
|
530
|
+
|
|
531
|
+
function quoteKey(name: string) {
|
|
532
|
+
if (UPPERCASE_REGEX.test(name) || RESERVED_KEYWORDS.has(name)) {
|
|
533
|
+
return options.quoteChar + name + options.quoteChar;
|
|
534
|
+
}
|
|
535
|
+
return name;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function handleValue(value: any, ctx: BuildContext) {
|
|
539
|
+
ctx.values.push(value);
|
|
540
|
+
return options.placeholder.replace("%d", String(ctx.values.length));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
class RawStatement extends Statement {
|
|
544
|
+
constructor(private readonly text: string) {
|
|
545
|
+
super();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
override computeParts(): Array<string | undefined> {
|
|
549
|
+
return [this.text];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
sql.rawStatement = (text: string) => new RawStatement(text);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IncomingHttpHeaders,
|
|
3
|
+
IncomingMessage,
|
|
4
|
+
ServerResponse,
|
|
5
|
+
} from "node:http";
|
|
6
|
+
import { Readable, type Transform } from "node:stream";
|
|
7
|
+
import {
|
|
8
|
+
createBrotliCompress,
|
|
9
|
+
createDeflate,
|
|
10
|
+
createGzip,
|
|
11
|
+
createZstdCompress,
|
|
12
|
+
} from "node:zlib";
|
|
13
|
+
|
|
14
|
+
function toHeaders(nodeHeaders: IncomingHttpHeaders) {
|
|
15
|
+
const headers = new Headers();
|
|
16
|
+
for (const [key, value] of Object.entries(nodeHeaders)) {
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
for (const val of value) {
|
|
19
|
+
headers.append(key, val);
|
|
20
|
+
}
|
|
21
|
+
} else if (value !== undefined) {
|
|
22
|
+
headers.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return headers;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readBody(req: IncomingMessage) {
|
|
29
|
+
return new Promise<string>((resolve, reject) => {
|
|
30
|
+
const chunks: string[] = [];
|
|
31
|
+
req.setEncoding("utf8");
|
|
32
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
33
|
+
req.on("end", () => resolve(chunks.join()));
|
|
34
|
+
req.on("error", (err) => reject(err));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createNativeRequest(nodeReq: IncomingMessage) {
|
|
39
|
+
const req = Object.create(null);
|
|
40
|
+
|
|
41
|
+
req.url = nodeReq.url;
|
|
42
|
+
req.method = nodeReq.method;
|
|
43
|
+
req.headers = toHeaders(nodeReq.headers);
|
|
44
|
+
req.json = () => readBody(nodeReq).then(JSON.parse);
|
|
45
|
+
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
req.signal = controller.signal;
|
|
48
|
+
nodeReq.on("close", () => controller.abort());
|
|
49
|
+
|
|
50
|
+
return req as Request;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const AVAILABLE_COMPRESSION_ALGORITHMS = [
|
|
54
|
+
{
|
|
55
|
+
name: "zstd", // added in v22.15.0
|
|
56
|
+
createCompressor: createZstdCompress,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "brotli", // added in v10.16.0
|
|
60
|
+
createCompressor: createBrotliCompress,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "gzip",
|
|
64
|
+
createCompressor: createGzip,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "deflate",
|
|
68
|
+
createCompressor: createDeflate,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function writeResponse(
|
|
73
|
+
nodeReq: IncomingMessage,
|
|
74
|
+
nodeRes: ServerResponse,
|
|
75
|
+
res: Response,
|
|
76
|
+
) {
|
|
77
|
+
const isSSE = res.headers.get("content-type") === "text/event-stream";
|
|
78
|
+
const acceptEncoding = nodeReq.headers["accept-encoding"];
|
|
79
|
+
let compressor: Transform | undefined;
|
|
80
|
+
|
|
81
|
+
if (res.body && acceptEncoding && !isSSE) {
|
|
82
|
+
// TODO handle quality
|
|
83
|
+
// ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding
|
|
84
|
+
for (const { name, createCompressor } of AVAILABLE_COMPRESSION_ALGORITHMS) {
|
|
85
|
+
if (acceptEncoding.includes(name)) {
|
|
86
|
+
res.headers.set("content-encoding", name);
|
|
87
|
+
compressor = createCompressor();
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
nodeRes.setHeaders(res.headers);
|
|
94
|
+
nodeRes.writeHead(res.status);
|
|
95
|
+
|
|
96
|
+
if (!res.body) {
|
|
97
|
+
return nodeRes.end();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isSSE) {
|
|
101
|
+
nodeRes.flushHeaders();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (compressor) {
|
|
105
|
+
Readable.fromWeb(res.body).pipe(compressor).pipe(nodeRes);
|
|
106
|
+
} else {
|
|
107
|
+
Readable.fromWeb(res.body).pipe(nodeRes);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function toNodeHandler(
|
|
112
|
+
nativeHandler: (req: Request) => Promise<Response>,
|
|
113
|
+
): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
|
|
114
|
+
return async function (nodeReq, nodeRes) {
|
|
115
|
+
const req = createNativeRequest(nodeReq);
|
|
116
|
+
|
|
117
|
+
const httpResponse = await nativeHandler(req);
|
|
118
|
+
|
|
119
|
+
writeResponse(nodeReq, nodeRes, httpResponse);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Brand<K, T> = K & { __brand: T };
|