tantan-typeorm-gs 1.0.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 +21 -0
- package/README.md +31 -0
- package/bun.lock +300 -0
- package/docs/authentication.md +118 -0
- package/docs/configuration.md +680 -0
- package/docs/contributing.md +177 -0
- package/docs/crud.md +351 -0
- package/docs/custom-client.md +165 -0
- package/docs/development.md +243 -0
- package/docs/entities.md +502 -0
- package/docs/find-options.md +296 -0
- package/docs/google-cloud.md +101 -0
- package/docs/installation.md +51 -0
- package/docs/integration-testing.md +178 -0
- package/docs/limitations.md +223 -0
- package/docs/pagination.md +384 -0
- package/docs/performance.md +171 -0
- package/docs/security.md +153 -0
- package/docs/sorting.md +0 -0
- package/docs/supported-features.md +310 -0
- package/docs/synchronization.md +203 -0
- package/package.json +43 -0
- package/src/client/index.ts +589 -0
- package/src/core/data-source.ts +47 -0
- package/src/core/driver.ts +290 -0
- package/src/core/error.ts +144 -0
- package/src/core/memory.ts +152 -0
- package/src/core/query/interpreter.ts +949 -0
- package/src/core/query/runner.ts +1297 -0
- package/src/core/query/types.ts +155 -0
- package/src/core/schema-builder.ts +64 -0
- package/src/core/types.ts +113 -0
- package/src/core/utils.ts +13 -0
- package/src/index.ts +3 -0
- package/tantan-typeorm-gs.code-workspace +8 -0
- package/test/base/0001-data-source.test.ts +252 -0
- package/test/base/0002-operator.test.ts +616 -0
- package/test/base/0003-select.test.ts +281 -0
- package/test/base/0004-aggregate.test.ts +213 -0
- package/test/base/0005-transcation.test.ts +1566 -0
- package/test/base/0006-relation.test.ts +1611 -0
- package/test/base/0007-logging.test.ts +182 -0
- package/test/base/0008-soft-delete.test.ts +649 -0
- package/test/google-sheets/0001-client.test.ts +1705 -0
- package/test/google-sheets/0002-worksheet-management.test.ts +408 -0
- package/test/google-sheets/0003-data-source.test.ts +1935 -0
- package/test/google-sheets/0004-transactions.test.ts +952 -0
- package/test/google-sheets/0005-operator.test.ts +1124 -0
- package/test/google-sheets/0006-object-criteria.test.ts +283 -0
- package/test/google-sheets/0007-performance.test.ts +532 -0
- package/test/public-api.test.ts +34 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AggregateColumn,
|
|
3
|
+
AggregateFunction,
|
|
4
|
+
DeleteQuery,
|
|
5
|
+
FromQuery,
|
|
6
|
+
InsertQuery,
|
|
7
|
+
JoinCondition,
|
|
8
|
+
JoinQuery,
|
|
9
|
+
JoinType,
|
|
10
|
+
OrderBy,
|
|
11
|
+
OrderDirection,
|
|
12
|
+
Pagination,
|
|
13
|
+
SelectColumn,
|
|
14
|
+
UpdateQuery,
|
|
15
|
+
WhereExpression,
|
|
16
|
+
WhereOperator
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
import { GoogleSheetsParseError, GoogleSheetsUnsupportedOperationError } from "../error.js";
|
|
19
|
+
|
|
20
|
+
export class GoogleSheetsQueryInterpreter
|
|
21
|
+
{
|
|
22
|
+
parseInsertQuery(query: string): InsertQuery
|
|
23
|
+
{
|
|
24
|
+
const match = query.match(/^INSERT\s+INTO\s+"([^"]+)"\s*\((.+?)\)\s*VALUES\s*(.+)$/i);
|
|
25
|
+
|
|
26
|
+
if (!match?.[1] || !match[2] || !match[3])
|
|
27
|
+
{
|
|
28
|
+
throw new GoogleSheetsParseError(`Unable to parse INSERT query: ${query}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const columns = match[2].split(',').map((column) => column.trim().replace(/^"|"$/g, ''));
|
|
32
|
+
const valuesPart = match[3].trim();
|
|
33
|
+
const rows: string[][] = [];
|
|
34
|
+
|
|
35
|
+
for (const rowMatch of valuesPart.matchAll(/\(([^()]*)\)/g))
|
|
36
|
+
{
|
|
37
|
+
const row = rowMatch[1];
|
|
38
|
+
|
|
39
|
+
if (row === undefined) continue;
|
|
40
|
+
|
|
41
|
+
rows.push(row.split(',').map((value) => value.trim()));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (rows.length === 0) throw new GoogleSheetsParseError(`Unable to parse INSERT query: ${query}`);
|
|
45
|
+
|
|
46
|
+
for (const values of rows)
|
|
47
|
+
{
|
|
48
|
+
if (columns.length !== values.length)
|
|
49
|
+
{
|
|
50
|
+
throw new GoogleSheetsParseError('INSERT columns and values count do not match.')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { columns, values: rows };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
parseUpdateQuery(query: string): UpdateQuery
|
|
58
|
+
{
|
|
59
|
+
const match = query.match(/^UPDATE\s+"([^"]+)"\s+SET\s+(.+?)\s+WHERE\s+([\s\S]+)$/i);
|
|
60
|
+
|
|
61
|
+
if (!match?.[1] || !match[2] || !match[3])
|
|
62
|
+
{
|
|
63
|
+
throw new GoogleSheetsParseError(`Unable to parse UPDATE query: ${query}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const assignments =
|
|
67
|
+
match[2]
|
|
68
|
+
.split(',')
|
|
69
|
+
.map(
|
|
70
|
+
(assignment) =>
|
|
71
|
+
{
|
|
72
|
+
const assignmentMatch =
|
|
73
|
+
assignment
|
|
74
|
+
.match(/^\s*"([^"]+)"\s*=\s*(CURRENT_TIMESTAMP|NULL|:[a-zA-Z_][a-zA-Z0-9_]*)\s*$/i);
|
|
75
|
+
|
|
76
|
+
if (!assignmentMatch?.[1] || !assignmentMatch[2])
|
|
77
|
+
{
|
|
78
|
+
throw new GoogleSheetsParseError(`Unable to parse UPDATE assignment: ${assignment}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { column: assignmentMatch[1], value: assignmentMatch[2] };
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
table: match[1],
|
|
87
|
+
assignments,
|
|
88
|
+
where: this.parseWhereExpressionPart(match[3].trim()),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
parseDeleteQuery(query: string): DeleteQuery
|
|
93
|
+
{
|
|
94
|
+
const match = query.match(/^DELETE\s+FROM\s+"([^"]+)"\s+WHERE\s+([\s\S]+)$/i);
|
|
95
|
+
|
|
96
|
+
if (!match?.[1] || !match[2]) throw new GoogleSheetsParseError(`Unable to parse DELETE query: ${query}`)
|
|
97
|
+
|
|
98
|
+
return { table: match[1], where: this.parseWhereExpressionPart(match[2].trim()) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
parseSelectColumns(query: string): SelectColumn[]
|
|
102
|
+
{
|
|
103
|
+
const selectMatch = query.match(/^SELECT\s+(.+?)\s+FROM\s+/i);
|
|
104
|
+
|
|
105
|
+
if (!selectMatch?.[1]) throw new GoogleSheetsParseError('Unable to parse SELECT clause.')
|
|
106
|
+
|
|
107
|
+
return selectMatch[1]
|
|
108
|
+
.split(',')
|
|
109
|
+
.map(
|
|
110
|
+
(expression) =>
|
|
111
|
+
{
|
|
112
|
+
const normalizedExpression = expression.trim();
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Literal:
|
|
116
|
+
*
|
|
117
|
+
* 1 AS "count"
|
|
118
|
+
*/
|
|
119
|
+
const literalMatch = normalizedExpression.match(/^(\d+)\s+AS\s+"([^"]+)"$/i);
|
|
120
|
+
|
|
121
|
+
if (literalMatch?.[1] && literalMatch[2])
|
|
122
|
+
{
|
|
123
|
+
return {
|
|
124
|
+
tableAlias: '',
|
|
125
|
+
column: literalMatch[1],
|
|
126
|
+
alias: literalMatch[2],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Aggregate:
|
|
132
|
+
*
|
|
133
|
+
* COUNT(*)
|
|
134
|
+
* COUNT("product"."category")
|
|
135
|
+
* SUM("product"."price")
|
|
136
|
+
* AVG("product"."price")
|
|
137
|
+
* MIN("product"."price")
|
|
138
|
+
* MAX("product"."price")
|
|
139
|
+
*
|
|
140
|
+
* AS "alias"
|
|
141
|
+
*/
|
|
142
|
+
const aggregateMatch =
|
|
143
|
+
normalizedExpression.match(
|
|
144
|
+
/^(COUNT|SUM|AVG|MIN|MAX)\s*\(\s*(\*|"[^"]+"\."[^"]+")\s*\)\s+AS\s+"([^"]+)"$/i,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (aggregateMatch?.[1] && aggregateMatch[2] && aggregateMatch[3])
|
|
148
|
+
{
|
|
149
|
+
let tableAlias = '';
|
|
150
|
+
|
|
151
|
+
const argumentMatch = aggregateMatch[2].match(/^"([^"]+)"\."([^"]+)"$/i);
|
|
152
|
+
|
|
153
|
+
if (argumentMatch?.[1]) tableAlias = argumentMatch[1];
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
tableAlias,
|
|
157
|
+
column: aggregateMatch[2],
|
|
158
|
+
alias: aggregateMatch[3],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* DISTINCT column without alias:
|
|
164
|
+
*
|
|
165
|
+
* DISTINCT "distinctAlias"."user_id"
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
const distinctMatch = normalizedExpression.match(/^DISTINCT\s+"([^"]+)"\."([^"]+)"$/i);
|
|
169
|
+
|
|
170
|
+
if (distinctMatch?.[1] && distinctMatch[2])
|
|
171
|
+
{
|
|
172
|
+
return {
|
|
173
|
+
tableAlias: distinctMatch[1],
|
|
174
|
+
column: distinctMatch[2],
|
|
175
|
+
alias: distinctMatch[2],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Column without alias:
|
|
181
|
+
*
|
|
182
|
+
* "distinctAlias"."user_id"
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
const columnMatch = normalizedExpression.match(/^"([^"]+)"\."([^"]+)"$/i);
|
|
186
|
+
|
|
187
|
+
if (columnMatch?.[1] && columnMatch[2])
|
|
188
|
+
{
|
|
189
|
+
return {
|
|
190
|
+
tableAlias: columnMatch[1],
|
|
191
|
+
column: columnMatch[2],
|
|
192
|
+
alias: columnMatch[2],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Normal column:
|
|
198
|
+
*
|
|
199
|
+
* "product"."id" AS "id"
|
|
200
|
+
*
|
|
201
|
+
* DISTINCT "product"."id" AS "id"
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
const match = normalizedExpression.match(/^(?:DISTINCT\s+)?"([^"]+)"\."([^"]+)"\s+AS\s+"([^"]+)"$/i);
|
|
205
|
+
|
|
206
|
+
if (!match?.[1] || !match[2] || !match[3])
|
|
207
|
+
{
|
|
208
|
+
throw new GoogleSheetsParseError(`Unable to parse SELECT expression: ${expression}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
tableAlias: match[1],
|
|
213
|
+
column: match[2],
|
|
214
|
+
alias: match[3],
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
parseFrom(query: string): FromQuery
|
|
221
|
+
{
|
|
222
|
+
const match = query.match(/\bFROM\s+"([^"]+)"\s+"([^"]+)"/i);
|
|
223
|
+
|
|
224
|
+
if (!match?.[1] || !match[2]) throw new GoogleSheetsParseError(`Unable to parse FROM clause: ${query}`)
|
|
225
|
+
|
|
226
|
+
return { table: match[1], alias: match[2] };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
parseJoins(query: string): JoinQuery[]
|
|
230
|
+
{
|
|
231
|
+
const joins: JoinQuery[] = [];
|
|
232
|
+
|
|
233
|
+
const regex =
|
|
234
|
+
/\b(LEFT|INNER)\s+JOIN\s+"([^"]+)"\s+"([^"]+)"\s+ON\s+([\s\S]*?)(?=\s+(?:LEFT|INNER)\s+JOIN\b|\s+WHERE\b|\s+ORDER\s+BY\b|\s+LIMIT\b|\s+OFFSET\b|$)/gi;
|
|
235
|
+
|
|
236
|
+
let match: RegExpExecArray | null;
|
|
237
|
+
|
|
238
|
+
while ((match = regex.exec(query)) !== null)
|
|
239
|
+
{
|
|
240
|
+
const [, type, table, alias, onExpression] = match;
|
|
241
|
+
|
|
242
|
+
if (!type || !table || !alias || !onExpression) continue;
|
|
243
|
+
|
|
244
|
+
const conditions = this.parseJoinConditions(onExpression);
|
|
245
|
+
|
|
246
|
+
if (conditions.length === 0) continue;
|
|
247
|
+
|
|
248
|
+
joins.push({
|
|
249
|
+
type: type.toUpperCase() as JoinType,
|
|
250
|
+
table,
|
|
251
|
+
alias,
|
|
252
|
+
conditions,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return joins;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private parseJoinConditions(expression: string): JoinCondition[]
|
|
260
|
+
{
|
|
261
|
+
let normalized = expression.trim();
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Remove wrapping parentheses.
|
|
265
|
+
*
|
|
266
|
+
* (
|
|
267
|
+
* "a"."x" = :param
|
|
268
|
+
* AND
|
|
269
|
+
* "a"."y" = "b"."y"
|
|
270
|
+
* )
|
|
271
|
+
*/
|
|
272
|
+
while (normalized.startsWith('(') && normalized.endsWith(')'))
|
|
273
|
+
{
|
|
274
|
+
normalized = normalized.slice(1, -1).trim();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const parts = normalized.split(/\s+AND\s+/i);
|
|
278
|
+
const conditions: JoinCondition[] = [];
|
|
279
|
+
|
|
280
|
+
for (const part of parts)
|
|
281
|
+
{
|
|
282
|
+
const condition = part.trim();
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Column = Column
|
|
286
|
+
*
|
|
287
|
+
* "a"."x" = "b"."y"
|
|
288
|
+
*/
|
|
289
|
+
|
|
290
|
+
const columnMatch = condition.match(/^"([^"]+)"\."([^"]+)"\s*=\s*"([^"]+)"\."([^"]+)"$/i);
|
|
291
|
+
|
|
292
|
+
if (columnMatch?.[1] && columnMatch[2] && columnMatch[3] && columnMatch[4])
|
|
293
|
+
{
|
|
294
|
+
conditions.push({
|
|
295
|
+
left: {
|
|
296
|
+
type: 'column',
|
|
297
|
+
tableAlias: columnMatch[1],
|
|
298
|
+
column: columnMatch[2],
|
|
299
|
+
},
|
|
300
|
+
operator: '=',
|
|
301
|
+
right: {
|
|
302
|
+
type: 'column',
|
|
303
|
+
tableAlias: columnMatch[3],
|
|
304
|
+
column: columnMatch[4],
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Column = Parameter
|
|
313
|
+
*
|
|
314
|
+
* "a"."x" = :orm_param_0
|
|
315
|
+
*/
|
|
316
|
+
const parameterMatch = condition.match(/^"([^"]+)"\."([^"]+)"\s*=\s*(:[a-zA-Z_][a-zA-Z0-9_]*)$/i);
|
|
317
|
+
|
|
318
|
+
if (parameterMatch?.[1] && parameterMatch[2] && parameterMatch[3])
|
|
319
|
+
{
|
|
320
|
+
conditions.push({
|
|
321
|
+
left: {
|
|
322
|
+
type: 'column',
|
|
323
|
+
tableAlias: parameterMatch[1],
|
|
324
|
+
column: parameterMatch[2],
|
|
325
|
+
},
|
|
326
|
+
operator: '=',
|
|
327
|
+
right: {
|
|
328
|
+
type: 'parameter',
|
|
329
|
+
value: parameterMatch[3],
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Column IS NULL
|
|
338
|
+
*
|
|
339
|
+
* ("a"."deletedAt" IS NULL)
|
|
340
|
+
*/
|
|
341
|
+
const nullMatch =
|
|
342
|
+
condition.match(
|
|
343
|
+
/^\("([^"]+)"\."([^"]+)"\s+IS\s+(NOT\s+)?NULL\)$/i,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (nullMatch?.[1] && nullMatch[2])
|
|
347
|
+
{
|
|
348
|
+
conditions.push({
|
|
349
|
+
left: {
|
|
350
|
+
type: 'column',
|
|
351
|
+
tableAlias: nullMatch[1],
|
|
352
|
+
column: nullMatch[2],
|
|
353
|
+
},
|
|
354
|
+
operator: nullMatch[3]
|
|
355
|
+
? 'IS NOT NULL'
|
|
356
|
+
: 'IS NULL',
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
throw new GoogleSheetsParseError(`Unable to parse JOIN condition: ${condition}`)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return conditions;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
parseAggregate(query: string): AggregateColumn | undefined
|
|
370
|
+
{
|
|
371
|
+
const match =
|
|
372
|
+
query.match(/^SELECT\s+(COUNT|SUM|AVG|MIN|MAX)\s*\(\s*([^)]+)\s*\)\s+AS\s+"([^"]+)"\s+FROM\s+/i);
|
|
373
|
+
|
|
374
|
+
if (!match?.[1] || !match[2] || !match[3]) return undefined;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
function: match[1].toUpperCase() as AggregateFunction,
|
|
378
|
+
argument: match[2].trim(),
|
|
379
|
+
alias: match[3],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
parseAggregates(query: string): AggregateColumn[]
|
|
384
|
+
{
|
|
385
|
+
const selectMatch = query.match(/^SELECT\s+(.+?)\s+FROM\s+/i);
|
|
386
|
+
|
|
387
|
+
if (!selectMatch?.[1]) return [];
|
|
388
|
+
|
|
389
|
+
const expressions = selectMatch[1].split(',').map((expression) => expression.trim());
|
|
390
|
+
|
|
391
|
+
const aggregates: AggregateColumn[] = [];
|
|
392
|
+
|
|
393
|
+
for (const expression of expressions)
|
|
394
|
+
{
|
|
395
|
+
const match = expression.match(/^(COUNT|SUM|AVG|MIN|MAX)\s*\(\s*([^)]+)\s*\)\s+AS\s+"([^"]+)"$/i);
|
|
396
|
+
|
|
397
|
+
if (!match?.[1] || !match[2] || !match[3]) continue;
|
|
398
|
+
|
|
399
|
+
aggregates.push({
|
|
400
|
+
function: match[1].toUpperCase() as AggregateFunction,
|
|
401
|
+
argument: match[2].trim(),
|
|
402
|
+
alias: match[3],
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return aggregates;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
parseWhereExpression(query: string): WhereExpression | undefined
|
|
410
|
+
{
|
|
411
|
+
const whereMatch = query.match(/\bWHERE\s+(.+?)(?=\s+ORDER\s+BY|\s+LIMIT\s+\d+|\s+OFFSET\s+\d+|$)/i);
|
|
412
|
+
|
|
413
|
+
if (!whereMatch?.[1]) return undefined;
|
|
414
|
+
|
|
415
|
+
return this.parseWhereExpressionPart(whereMatch[1].trim());
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private parseWhereExpressionPart(expression: string): WhereExpression
|
|
419
|
+
{
|
|
420
|
+
const normalizedExpression = this.unwrapOuterParentheses(expression.trim());
|
|
421
|
+
const orExpressions = this.splitLogicalExpression(normalizedExpression, 'OR');
|
|
422
|
+
|
|
423
|
+
if (orExpressions.length > 1)
|
|
424
|
+
{
|
|
425
|
+
return {
|
|
426
|
+
type: 'or',
|
|
427
|
+
expressions: orExpressions.map((part) => this.parseWhereAndExpression(part)),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return this.parseWhereAndExpression(normalizedExpression);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private parseWhereAndExpression(expression: string): WhereExpression
|
|
435
|
+
{
|
|
436
|
+
const normalizedExpression = this.unwrapOuterParentheses(expression.trim());
|
|
437
|
+
|
|
438
|
+
const notMatch = normalizedExpression.match(/^NOT\s*(\(.*\))$/is);
|
|
439
|
+
|
|
440
|
+
if (notMatch?.[1])
|
|
441
|
+
{
|
|
442
|
+
return {
|
|
443
|
+
type: 'not',
|
|
444
|
+
expression: this.parseWhereExpressionPart(notMatch[1])
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const andExpressions = this.splitLogicalExpression(normalizedExpression, 'AND');
|
|
449
|
+
|
|
450
|
+
if (andExpressions.length === 1) return this.parseSingleWhereCondition(andExpressions[0]!);
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
type: 'and',
|
|
454
|
+
expressions: andExpressions.map((part) => this.parseWhereExpressionPart(part))
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private parseSingleWhereCondition(expression: string): WhereExpression
|
|
459
|
+
{
|
|
460
|
+
const normalizedExpression = this.unwrapOuterParentheses(expression.trim());
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* IS NULL / IS NOT NULL
|
|
464
|
+
*/
|
|
465
|
+
const nullMatch = normalizedExpression.match(/^(?:"([^"]+)"\.)?"([^"]+)"\s+IS\s+(NOT\s+)?NULL$/i);
|
|
466
|
+
|
|
467
|
+
if (nullMatch?.[2])
|
|
468
|
+
{
|
|
469
|
+
return {
|
|
470
|
+
type: 'condition',
|
|
471
|
+
condition: {
|
|
472
|
+
tableAlias: nullMatch[1] ?? '',
|
|
473
|
+
column: nullMatch[2],
|
|
474
|
+
operator: nullMatch[3] ? 'IS NOT NULL' : 'IS NULL',
|
|
475
|
+
parameters: [],
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* IN / NOT IN
|
|
482
|
+
*/
|
|
483
|
+
const inMatch = normalizedExpression.match(/^(?:"([^"]+)"\.)?"([^"]+)"\s+(NOT\s+)?IN\s*\((.+)\)$/i);
|
|
484
|
+
|
|
485
|
+
if (inMatch?.[2] && inMatch[4])
|
|
486
|
+
{
|
|
487
|
+
return {
|
|
488
|
+
type: 'condition',
|
|
489
|
+
condition: {
|
|
490
|
+
tableAlias: inMatch[1] ?? '',
|
|
491
|
+
column: inMatch[2],
|
|
492
|
+
operator: inMatch[3] ? 'NOT IN' : 'IN',
|
|
493
|
+
parameters: inMatch[4].split(',').map(parameter => parameter.trim()),
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* BETWEEN
|
|
500
|
+
*/
|
|
501
|
+
const betweenMatch =
|
|
502
|
+
normalizedExpression.match(
|
|
503
|
+
/^(?:"([^"]+)"\.)?"([^"]+)"\s+BETWEEN\s+(:[a-zA-Z_][a-zA-Z0-9_]*)\s+AND\s+(:[a-zA-Z_][a-zA-Z0-9_]*)$/i
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (betweenMatch?.[2] && betweenMatch[3] && betweenMatch[4])
|
|
507
|
+
{
|
|
508
|
+
return {
|
|
509
|
+
type: 'condition',
|
|
510
|
+
condition: {
|
|
511
|
+
tableAlias: betweenMatch[1] ?? '',
|
|
512
|
+
column: betweenMatch[2],
|
|
513
|
+
operator: 'BETWEEN',
|
|
514
|
+
parameters: [
|
|
515
|
+
betweenMatch[3],
|
|
516
|
+
betweenMatch[4],
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* ANY
|
|
524
|
+
*
|
|
525
|
+
* "User"."id" = ANY(:orm_param_0)
|
|
526
|
+
*/
|
|
527
|
+
const anyMatch =
|
|
528
|
+
normalizedExpression.match(
|
|
529
|
+
/^(?:"([^"]+)"\.)?"([^"]+)"\s*=\s*ANY\s*\(\s*(:[a-zA-Z_][a-zA-Z0-9_]*)\s*\)$/i
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
if (anyMatch?.[2] && anyMatch[3])
|
|
533
|
+
{
|
|
534
|
+
return {
|
|
535
|
+
type: 'condition',
|
|
536
|
+
condition: {
|
|
537
|
+
tableAlias: anyMatch[1] ?? '',
|
|
538
|
+
column: anyMatch[2],
|
|
539
|
+
operator: 'ANY',
|
|
540
|
+
parameters: [anyMatch[3]],
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* LIKE / NOT LIKE
|
|
547
|
+
*
|
|
548
|
+
* ILIKE / NOT ILIKE
|
|
549
|
+
*/
|
|
550
|
+
const likeMatch =
|
|
551
|
+
normalizedExpression.match(
|
|
552
|
+
/^(?:"([^"]+)"\.)?"([^"]+)"\s+(NOT\s+)?(I?LIKE)\s+(:[a-zA-Z_][a-zA-Z0-9_]*)$/i
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
if (likeMatch?.[2] && likeMatch[4] && likeMatch[5])
|
|
556
|
+
{
|
|
557
|
+
const likeOperator = likeMatch[4].toUpperCase();
|
|
558
|
+
|
|
559
|
+
let operator: WhereOperator;
|
|
560
|
+
|
|
561
|
+
if (likeOperator === 'LIKE') operator = likeMatch[3] ? 'NOT LIKE' : 'LIKE';
|
|
562
|
+
else operator = likeMatch[3] ? 'NOT ILIKE' : 'ILIKE';
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
type: 'condition',
|
|
566
|
+
condition: {
|
|
567
|
+
tableAlias: likeMatch[1] ?? '',
|
|
568
|
+
column: likeMatch[2],
|
|
569
|
+
operator,
|
|
570
|
+
parameters: [likeMatch[5]],
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* =, !=, <>, >, >=, <, <=
|
|
577
|
+
*/
|
|
578
|
+
const comparisonMatch =
|
|
579
|
+
normalizedExpression.match(
|
|
580
|
+
/^(?:"([^"]+)"\.)?"([^"]+)"\s*(>=|<=|!=|<>|=|>|<)\s*(:[a-zA-Z_][a-zA-Z0-9_]*)$/i,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
if (comparisonMatch?.[2] && comparisonMatch[3] && comparisonMatch[4])
|
|
584
|
+
{
|
|
585
|
+
return {
|
|
586
|
+
type: 'condition',
|
|
587
|
+
condition: {
|
|
588
|
+
tableAlias: comparisonMatch[1] ?? '',
|
|
589
|
+
column: comparisonMatch[2],
|
|
590
|
+
operator: comparisonMatch[3] as WhereOperator,
|
|
591
|
+
parameters: [comparisonMatch[4]],
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
throw new GoogleSheetsParseError(`Unable to parse WHERE expression: ${expression}`)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private splitLogicalExpression(expression: string, operator: 'AND' | 'OR'): string[]
|
|
600
|
+
{
|
|
601
|
+
const parts: string[] = [];
|
|
602
|
+
|
|
603
|
+
let depth = 0;
|
|
604
|
+
let start = 0;
|
|
605
|
+
|
|
606
|
+
for (let index = 0; index < expression.length; index++)
|
|
607
|
+
{
|
|
608
|
+
const character = expression[index];
|
|
609
|
+
|
|
610
|
+
if (character === '(')
|
|
611
|
+
{
|
|
612
|
+
depth++;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (character === ')')
|
|
617
|
+
{
|
|
618
|
+
depth--;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (depth !== 0) continue;
|
|
623
|
+
|
|
624
|
+
const remaining = expression.slice(index);
|
|
625
|
+
|
|
626
|
+
const match = remaining.match(new RegExp(`^${operator}\\b`, 'i'));
|
|
627
|
+
|
|
628
|
+
if (!match) continue;
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* BETWEEN contains its own AND:
|
|
632
|
+
*
|
|
633
|
+
* id BETWEEN :min AND :max
|
|
634
|
+
*
|
|
635
|
+
* That AND is not a logical AND.
|
|
636
|
+
*/
|
|
637
|
+
|
|
638
|
+
if (operator === 'AND')
|
|
639
|
+
{
|
|
640
|
+
const before = expression.slice(start, index).trim();
|
|
641
|
+
|
|
642
|
+
if (/\bBETWEEN\s+:[a-zA-Z_][a-zA-Z0-9_]*$/i.test(before)) continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
parts.push(expression.slice(start, index).trim(),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
index += match[0].length - 1;
|
|
649
|
+
start = index + 1;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
parts.push(expression.slice(start).trim());
|
|
653
|
+
|
|
654
|
+
return parts.filter((part) => part.length > 0);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private unwrapOuterParentheses(expression: string): string
|
|
658
|
+
{
|
|
659
|
+
let result = expression.trim();
|
|
660
|
+
|
|
661
|
+
while (result.startsWith('(') && result.endsWith(')') && this.isWrappedByOuterParentheses(result))
|
|
662
|
+
{
|
|
663
|
+
result = result.slice(1, -1).trim();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private isWrappedByOuterParentheses(expression: string): boolean
|
|
670
|
+
{
|
|
671
|
+
let depth = 0;
|
|
672
|
+
|
|
673
|
+
for (let index = 0; index < expression.length; index++)
|
|
674
|
+
{
|
|
675
|
+
const character = expression[index];
|
|
676
|
+
|
|
677
|
+
if (character === '(') depth++;
|
|
678
|
+
else if (character === ')')
|
|
679
|
+
{
|
|
680
|
+
depth--;
|
|
681
|
+
|
|
682
|
+
if (depth === 0 && index !== expression.length - 1) return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return depth === 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
extractColumnName(expression: string): string
|
|
690
|
+
{
|
|
691
|
+
const match = expression.match(/^"[^"]+"\."([^"]+)"$/i);
|
|
692
|
+
|
|
693
|
+
if (match?.[1]) return match[1];
|
|
694
|
+
|
|
695
|
+
return expression.replace(/^"|"$/g, '');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
evaluateAggregate(rows: Record<string, unknown>[], aggregate: AggregateColumn): number | null
|
|
699
|
+
{
|
|
700
|
+
if (aggregate.function === 'COUNT')
|
|
701
|
+
{
|
|
702
|
+
/**
|
|
703
|
+
* COUNT(*) / COUNT(1)
|
|
704
|
+
*
|
|
705
|
+
* TypeORM uses COUNT(1) for repository.count().
|
|
706
|
+
* Both forms count rows, not a column value.
|
|
707
|
+
*/
|
|
708
|
+
|
|
709
|
+
if (aggregate.argument === '*' || /^\d+$/.test(aggregate.argument)) return rows.length;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const columnMatch = aggregate.argument.match(/^"?([^".]+)"?\."?([^".]+)"?$/);
|
|
713
|
+
const tableAlias = columnMatch?.[1];
|
|
714
|
+
const column = columnMatch?.[2] ?? this.extractColumnName(aggregate.argument);
|
|
715
|
+
|
|
716
|
+
const getValue = (row: Record<string, unknown>): unknown =>
|
|
717
|
+
{
|
|
718
|
+
if (tableAlias)
|
|
719
|
+
{
|
|
720
|
+
const tableRow = row[tableAlias];
|
|
721
|
+
|
|
722
|
+
if (tableRow && typeof tableRow === 'object') return (tableRow as Record<string, unknown>)[column];
|
|
723
|
+
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return row[column];
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const values = rows
|
|
731
|
+
.map((row) => getValue(row))
|
|
732
|
+
.filter((value): value is string | number => value !== null && value !== undefined);
|
|
733
|
+
|
|
734
|
+
if (aggregate.function === 'COUNT') return values.length;
|
|
735
|
+
if (values.length === 0) return null;
|
|
736
|
+
|
|
737
|
+
const numericValues = values
|
|
738
|
+
.map((value) => typeof value === 'number' ? value : Number(value))
|
|
739
|
+
.filter((value) => Number.isFinite(value));
|
|
740
|
+
|
|
741
|
+
if (numericValues.length === 0) return null;
|
|
742
|
+
|
|
743
|
+
switch (aggregate.function)
|
|
744
|
+
{
|
|
745
|
+
case 'SUM': return numericValues.reduce((sum, value) => sum + value, 0);
|
|
746
|
+
case 'AVG': return numericValues.reduce((sum, value) => sum + value, 0) / numericValues.length;
|
|
747
|
+
case 'MIN': return Math.min(...numericValues);
|
|
748
|
+
case 'MAX': return Math.max(...numericValues);
|
|
749
|
+
default: throw new GoogleSheetsUnsupportedOperationError(`Unsupported aggregate function: ${aggregate.function}`)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
evaluateWhere(value: unknown, operator: WhereOperator, parameters: unknown[]): boolean
|
|
754
|
+
{
|
|
755
|
+
switch (operator)
|
|
756
|
+
{
|
|
757
|
+
case '=': return this.compareValues(value, parameters[0]);
|
|
758
|
+
case '!=':
|
|
759
|
+
case '<>':
|
|
760
|
+
return value !== parameters[0];
|
|
761
|
+
case '>':
|
|
762
|
+
return value !== undefined &&
|
|
763
|
+
value !== null &&
|
|
764
|
+
parameters[0] !== undefined &&
|
|
765
|
+
parameters[0] !== null &&
|
|
766
|
+
value > parameters[0];
|
|
767
|
+
case '>=':
|
|
768
|
+
return value !== undefined &&
|
|
769
|
+
value !== null &&
|
|
770
|
+
parameters[0] !== undefined &&
|
|
771
|
+
parameters[0] !== null &&
|
|
772
|
+
value >= parameters[0];
|
|
773
|
+
case '<':
|
|
774
|
+
return value !== undefined &&
|
|
775
|
+
value !== null &&
|
|
776
|
+
parameters[0] !== undefined &&
|
|
777
|
+
parameters[0] !== null &&
|
|
778
|
+
value < parameters[0];
|
|
779
|
+
case '<=':
|
|
780
|
+
return value !== undefined &&
|
|
781
|
+
value !== null &&
|
|
782
|
+
parameters[0] !== undefined &&
|
|
783
|
+
parameters[0] !== null &&
|
|
784
|
+
value <= parameters[0];
|
|
785
|
+
case 'IS NULL': return (value === null || value === undefined);
|
|
786
|
+
case 'IS NOT NULL': return (value !== null && value !== undefined);
|
|
787
|
+
case 'IN': return parameters.some((parameter) => this.compareValues(value, parameter));
|
|
788
|
+
case 'NOT IN': return !parameters.some((parameter) => this.compareValues(value, parameter));
|
|
789
|
+
case 'ANY':
|
|
790
|
+
{
|
|
791
|
+
const values = parameters[0];
|
|
792
|
+
|
|
793
|
+
if (!Array.isArray(values)) return false;
|
|
794
|
+
|
|
795
|
+
return values.some(
|
|
796
|
+
(parameter) => this.compareValues(value, parameter),
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
case 'LIKE': return this.evaluateLike(value, parameters[0], false);
|
|
800
|
+
case 'NOT LIKE': return !this.evaluateLike(value, parameters[0], false);
|
|
801
|
+
case 'ILIKE': return this.evaluateLike(value, parameters[0], true);
|
|
802
|
+
case 'NOT ILIKE': return !this.evaluateLike(value, parameters[0], true);
|
|
803
|
+
case 'BETWEEN':
|
|
804
|
+
return value !== undefined &&
|
|
805
|
+
value !== null &&
|
|
806
|
+
parameters[0] !== undefined &&
|
|
807
|
+
parameters[0] !== null &&
|
|
808
|
+
parameters[1] !== undefined &&
|
|
809
|
+
parameters[1] !== null &&
|
|
810
|
+
value >= parameters[0] &&
|
|
811
|
+
value <= parameters[1];
|
|
812
|
+
default: throw new GoogleSheetsUnsupportedOperationError(`Unsupported operator: ${operator}`)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private evaluateLike(value: unknown, pattern: unknown, caseInsensitive: boolean): boolean
|
|
817
|
+
{
|
|
818
|
+
if (typeof value !== 'string' || typeof pattern !== 'string') return false;
|
|
819
|
+
|
|
820
|
+
const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
821
|
+
|
|
822
|
+
const regexPattern = `^${escapedPattern.replaceAll('%', '.*').replaceAll('_', '.')}$`;
|
|
823
|
+
|
|
824
|
+
return new RegExp(regexPattern, caseInsensitive ? 'i' : '').test(value);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
evaluateWhereExpression(
|
|
828
|
+
expression: WhereExpression,
|
|
829
|
+
resolveValue: (
|
|
830
|
+
tableAlias: string,
|
|
831
|
+
column: string,
|
|
832
|
+
) => unknown,
|
|
833
|
+
resolveParameter: (
|
|
834
|
+
parameter: string,
|
|
835
|
+
) => unknown,
|
|
836
|
+
): boolean
|
|
837
|
+
{
|
|
838
|
+
switch (expression.type)
|
|
839
|
+
{
|
|
840
|
+
case 'condition':
|
|
841
|
+
{
|
|
842
|
+
const condition = expression.condition;
|
|
843
|
+
|
|
844
|
+
const parameters = condition.parameters.map((parameter) => resolveParameter(parameter)).flat();
|
|
845
|
+
|
|
846
|
+
return this.evaluateWhere(
|
|
847
|
+
resolveValue(
|
|
848
|
+
condition.tableAlias,
|
|
849
|
+
condition.column,
|
|
850
|
+
),
|
|
851
|
+
condition.operator,
|
|
852
|
+
parameters,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
case 'and':
|
|
857
|
+
return expression.expressions.every(
|
|
858
|
+
(expression) => this.evaluateWhereExpression(expression, resolveValue, resolveParameter)
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
case 'or':
|
|
862
|
+
return expression.expressions.some(
|
|
863
|
+
(expression) => this.evaluateWhereExpression(expression, resolveValue, resolveParameter)
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
case 'not': return !this.evaluateWhereExpression(expression.expression, resolveValue, resolveParameter);
|
|
867
|
+
default: throw new GoogleSheetsUnsupportedOperationError(`Unsupported operator: ${expression}`)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private compareValues(left: unknown, right: unknown): boolean
|
|
872
|
+
{
|
|
873
|
+
if (left === null || left === undefined || right === null || right === undefined)
|
|
874
|
+
{
|
|
875
|
+
return left === right;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (typeof left === 'number' && typeof right === 'string')
|
|
879
|
+
{
|
|
880
|
+
return left === Number(right);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (typeof left === 'string' && typeof right === 'number')
|
|
884
|
+
{
|
|
885
|
+
return Number(left) === right;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return left === right;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
parseOrderBy(query: string): OrderBy[]
|
|
892
|
+
{
|
|
893
|
+
const match = query.match(/\bORDER\s+BY\s+(.+?)(?:\s+LIMIT\s+\d+|\s+OFFSET\s+\d+|$)/i);
|
|
894
|
+
|
|
895
|
+
if (!match?.[1]) return [];
|
|
896
|
+
|
|
897
|
+
return match[1]
|
|
898
|
+
.split(',')
|
|
899
|
+
.map(
|
|
900
|
+
(expression) =>
|
|
901
|
+
{
|
|
902
|
+
const trimmedExpression = expression.trim();
|
|
903
|
+
|
|
904
|
+
// ORDER BY "table"."column" ASC
|
|
905
|
+
const columnMatch = trimmedExpression.match(/^"([^"]+)"\."([^"]+)"(?:\s+(ASC|DESC))?$/i);
|
|
906
|
+
|
|
907
|
+
if (columnMatch?.[1] && columnMatch[2])
|
|
908
|
+
{
|
|
909
|
+
return {
|
|
910
|
+
tableAlias: columnMatch[1],
|
|
911
|
+
column: columnMatch[2],
|
|
912
|
+
direction: (columnMatch[3]?.toUpperCase() ?? 'ASC') as OrderDirection,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ORDER BY "table_column" ASC
|
|
917
|
+
const aliasMatch = trimmedExpression.match(/^"([^"]+)"(?:\s+(ASC|DESC))?$/i);
|
|
918
|
+
|
|
919
|
+
if (aliasMatch?.[1])
|
|
920
|
+
{
|
|
921
|
+
const alias = aliasMatch[1];
|
|
922
|
+
const separatorIndex = alias.lastIndexOf('_');
|
|
923
|
+
|
|
924
|
+
if (separatorIndex > 0)
|
|
925
|
+
{
|
|
926
|
+
return {
|
|
927
|
+
tableAlias: alias.slice(0, separatorIndex),
|
|
928
|
+
column: alias.slice(separatorIndex + 1),
|
|
929
|
+
direction: (aliasMatch[2]?.toUpperCase() ?? 'ASC') as OrderDirection
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
throw new GoogleSheetsParseError(`Unable to parse ORDER BY expression: ${expression}`)
|
|
935
|
+
},
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
parsePagination(query: string): Pagination
|
|
940
|
+
{
|
|
941
|
+
const limitMatch = query.match(/\bLIMIT\s+(\d+)/i);
|
|
942
|
+
const offsetMatch = query.match(/\bOFFSET\s+(\d+)/i);
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
limit: limitMatch ? Number(limitMatch[1]) : undefined,
|
|
946
|
+
offset: offsetMatch ? Number(offsetMatch[1]) : undefined,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|