@graphoria/server 0.1.1 → 0.1.3
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/dist/src/configuration/getSchemas/index.d.ts +4 -4
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +2 -2
- package/dist/src/configuration/index.d.ts +2 -2
- package/dist/src/databases/common.d.ts +18 -10
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/getStructure.d.ts +0 -2
- package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getStructure.d.ts +0 -2
- package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getStructure.d.ts +0 -2
- package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
- package/dist/src/databases/high-level-operations.d.ts +1 -3
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/metadata/structure.d.ts +0 -2
- package/dist/src/databases/metadata/structure.d.ts.map +1 -1
- package/dist/src/databases/schemaBuilder/generateInsert/index.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +2 -2
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/types/zod/db.d.ts +0 -5
- package/dist/src/types/zod/db.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/playgrounds/graphiql/index.html +1 -1
- package/src/databases/common.ts +49 -34
- package/src/databases/engines/mssql/connection.ts +2 -0
- package/src/databases/engines/mssql/query/index.ts +34 -21
- package/src/databases/engines/mysql/connection.ts +1 -1
- package/src/databases/engines/mysql/query/index.ts +19 -18
- package/src/databases/engines/postgresql/connection.ts +1 -1
- package/src/databases/engines/postgresql/query/index.ts +0 -3
- package/src/databases/schemaBuilder/generateInsert/index.ts +6 -21
- package/src/databases/transformers/data-transformers.ts +3 -0
- package/src/index.ts +0 -2
- package/src/types/zod/db.ts +0 -2
package/src/databases/common.ts
CHANGED
|
@@ -47,7 +47,6 @@ export const buildConditions = (
|
|
|
47
47
|
tableAlias: string,
|
|
48
48
|
level: number,
|
|
49
49
|
aliasMap: { [alias: string]: string },
|
|
50
|
-
quoted = false,
|
|
51
50
|
): string => {
|
|
52
51
|
if (!whereArgs) return "";
|
|
53
52
|
|
|
@@ -71,7 +70,7 @@ export const buildConditions = (
|
|
|
71
70
|
? `${mappingDbTypeCharVar[dbType]}${variablesDefinition.findIndex((v) => v.name === operand.substring(1)) + 1}`
|
|
72
71
|
: operand;
|
|
73
72
|
|
|
74
|
-
const condition = buildCondition(tableAlias, fieldName, operator, resolvedValue,
|
|
73
|
+
const condition = buildCondition(tableAlias, fieldName, operator, resolvedValue, dbType);
|
|
75
74
|
|
|
76
75
|
if (condition) {
|
|
77
76
|
conditions.push(condition);
|
|
@@ -88,7 +87,7 @@ export const buildConditions = (
|
|
|
88
87
|
fieldName,
|
|
89
88
|
tableAlias,
|
|
90
89
|
nestedTableAlias,
|
|
91
|
-
|
|
90
|
+
dbType,
|
|
92
91
|
);
|
|
93
92
|
|
|
94
93
|
const nestedConditions = buildConditions(
|
|
@@ -100,12 +99,11 @@ export const buildConditions = (
|
|
|
100
99
|
nestedTableAlias,
|
|
101
100
|
level + 1,
|
|
102
101
|
aliasMap,
|
|
103
|
-
quoted,
|
|
104
102
|
);
|
|
105
103
|
|
|
106
104
|
const existsClause = `EXISTS (
|
|
107
105
|
SELECT 1
|
|
108
|
-
FROM ${entities.queriesMap[fieldName]!.
|
|
106
|
+
FROM ${entities.queriesMap[fieldName]!.dottedQuotedName} ${nestedTableAlias}
|
|
109
107
|
WHERE ${joinCondition}${nestedConditions ? ` AND (${nestedConditions})` : ""}
|
|
110
108
|
)`;
|
|
111
109
|
|
|
@@ -116,7 +114,33 @@ export const buildConditions = (
|
|
|
116
114
|
return conditions.length > 0 ? conditions.join(" AND ") : "";
|
|
117
115
|
};
|
|
118
116
|
|
|
119
|
-
|
|
117
|
+
// Reserved words (e.g. PRINT, ORDER) are valid GraphQL names; always delimit
|
|
118
|
+
// identifiers so they parse as identifiers in every dialect.
|
|
119
|
+
const identifierDelimiters: Record<DatabaseType, [string, string]> = {
|
|
120
|
+
pg: ['"', '"'],
|
|
121
|
+
mysql: ["`", "`"],
|
|
122
|
+
mssql: ["[", "]"],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const wrapIdentifierFp =
|
|
126
|
+
(dbType: DatabaseType) =>
|
|
127
|
+
(value: string): string => {
|
|
128
|
+
const [open, close] = identifierDelimiters[dbType];
|
|
129
|
+
// Doubling the closing delimiter is the escape in all three dialects; only
|
|
130
|
+
// introspected names can contain one (GraphQL names never do).
|
|
131
|
+
return `${open}${value.replaceAll(close, `${close}${close}`)}${close}`;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const wrapIdentifierPG = wrapIdentifierFp("pg");
|
|
135
|
+
export const wrapIdentifierMSSQL = wrapIdentifierFp("mssql");
|
|
136
|
+
export const wrapIdentifierMySQL = wrapIdentifierFp("mysql");
|
|
137
|
+
|
|
138
|
+
export const qualifiedNameFp =
|
|
139
|
+
(dbType: DatabaseType) =>
|
|
140
|
+
({ schema, name }: { schema: string; name: string }): string => {
|
|
141
|
+
const wrap = wrapIdentifierFp(dbType);
|
|
142
|
+
return `${wrap(schema)}.${wrap(name)}`;
|
|
143
|
+
};
|
|
120
144
|
|
|
121
145
|
// Helper to resolve variable values
|
|
122
146
|
const resolveVariable = <T>(value: unknown, variables: Record<string, unknown>): T => {
|
|
@@ -131,9 +155,9 @@ const buildCondition = (
|
|
|
131
155
|
field: string,
|
|
132
156
|
operator: string,
|
|
133
157
|
value: unknown,
|
|
134
|
-
|
|
158
|
+
dbType: DatabaseType,
|
|
135
159
|
): string | null => {
|
|
136
|
-
const wrappedField =
|
|
160
|
+
const wrappedField = wrapIdentifierFp(dbType)(field);
|
|
137
161
|
|
|
138
162
|
switch (operator) {
|
|
139
163
|
case "eq":
|
|
@@ -174,14 +198,13 @@ const pairsToAnd = (
|
|
|
174
198
|
parentColumn: string;
|
|
175
199
|
childColumn: string;
|
|
176
200
|
}[],
|
|
177
|
-
|
|
178
|
-
) =>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
`${p.parentAlias}.${wrapIf(p.parentColumn, quoted)} = ${p.childAlias}.${wrapIf(p.childColumn, quoted)}`,
|
|
183
|
-
)
|
|
201
|
+
dbType: DatabaseType,
|
|
202
|
+
) => {
|
|
203
|
+
const wrap = wrapIdentifierFp(dbType);
|
|
204
|
+
return pairs
|
|
205
|
+
.map((p) => `${p.parentAlias}.${wrap(p.parentColumn)} = ${p.childAlias}.${wrap(p.childColumn)}`)
|
|
184
206
|
.join(" AND ");
|
|
207
|
+
};
|
|
185
208
|
|
|
186
209
|
export const findJoinCondition = (
|
|
187
210
|
entities: MergedEntities,
|
|
@@ -189,7 +212,7 @@ export const findJoinCondition = (
|
|
|
189
212
|
childTableName: string,
|
|
190
213
|
parentAlias: string,
|
|
191
214
|
childAlias: string,
|
|
192
|
-
|
|
215
|
+
dbType: DatabaseType,
|
|
193
216
|
): string => {
|
|
194
217
|
const relations = entities.getForeignKeysBetweenTables(parentTableName, childTableName);
|
|
195
218
|
|
|
@@ -204,7 +227,7 @@ export const findJoinCondition = (
|
|
|
204
227
|
parentColumn: c.source,
|
|
205
228
|
childColumn: c.target,
|
|
206
229
|
})),
|
|
207
|
-
|
|
230
|
+
dbType,
|
|
208
231
|
),
|
|
209
232
|
) ?? []),
|
|
210
233
|
...(relations.relationshipsReversed?.map((relation) =>
|
|
@@ -215,7 +238,7 @@ export const findJoinCondition = (
|
|
|
215
238
|
parentColumn: c.target,
|
|
216
239
|
childColumn: c.source,
|
|
217
240
|
})),
|
|
218
|
-
|
|
241
|
+
dbType,
|
|
219
242
|
),
|
|
220
243
|
) ?? []),
|
|
221
244
|
].join(" AND ");
|
|
@@ -233,7 +256,6 @@ export const buildWhereClauseFp =
|
|
|
233
256
|
parentTableAlias: string | null,
|
|
234
257
|
level: number,
|
|
235
258
|
aliasMap: { [alias: string]: string },
|
|
236
|
-
quoted = false,
|
|
237
259
|
): string => {
|
|
238
260
|
const whereConditions: string[] = [
|
|
239
261
|
buildConditions(
|
|
@@ -245,7 +267,6 @@ export const buildWhereClauseFp =
|
|
|
245
267
|
tableAlias,
|
|
246
268
|
level + 1,
|
|
247
269
|
aliasMap,
|
|
248
|
-
quoted,
|
|
249
270
|
),
|
|
250
271
|
parentTableName && parentTableAlias
|
|
251
272
|
? findJoinCondition(
|
|
@@ -254,7 +275,7 @@ export const buildWhereClauseFp =
|
|
|
254
275
|
field.name,
|
|
255
276
|
parentTableAlias,
|
|
256
277
|
tableAlias,
|
|
257
|
-
|
|
278
|
+
dbType,
|
|
258
279
|
)
|
|
259
280
|
: null,
|
|
260
281
|
].filter(Boolean) as string[];
|
|
@@ -282,6 +303,7 @@ const parseOrderDirection = (direction: string): { sort: string; nulls?: string
|
|
|
282
303
|
export const buildOrderByClauseFp =
|
|
283
304
|
(dbType: DatabaseType = "pg") =>
|
|
284
305
|
(entities: MergedEntities, field: SelectionAnalysis, tableAlias: string): string => {
|
|
306
|
+
const wrap = wrapIdentifierFp(dbType);
|
|
285
307
|
if (field.arguments?.["orderBy"]) {
|
|
286
308
|
const orderBy = field.arguments?.["orderBy"];
|
|
287
309
|
// orderBy is an array or object of { column: direction } - no variable resolution needed
|
|
@@ -306,11 +328,11 @@ export const buildOrderByClauseFp =
|
|
|
306
328
|
if (nulls) {
|
|
307
329
|
if (dbType === "pg") {
|
|
308
330
|
// PostgreSQL supports NULLS FIRST/LAST natively
|
|
309
|
-
return `${tableAlias}
|
|
331
|
+
return `${tableAlias}.${wrap(colName)} ${sort} NULLS ${nulls}`;
|
|
310
332
|
} else if (dbType === "mysql") {
|
|
311
333
|
// MySQL: Use CASE statement for NULL handling
|
|
312
334
|
const nullFirst = nulls === "FIRST";
|
|
313
|
-
return `CASE WHEN ${tableAlias}.${colName} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${colName} ${sort}`;
|
|
335
|
+
return `CASE WHEN ${tableAlias}.${wrap(colName)} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${wrap(colName)} ${sort}`;
|
|
314
336
|
} else {
|
|
315
337
|
// MSSQL requires CASE statements for NULL handling
|
|
316
338
|
const nullFirst = nulls === "FIRST";
|
|
@@ -318,10 +340,10 @@ export const buildOrderByClauseFp =
|
|
|
318
340
|
|
|
319
341
|
if ((nullFirst && isAsc) || (!nullFirst && !isAsc)) {
|
|
320
342
|
// NULLs naturally come first in this combination
|
|
321
|
-
return `${tableAlias}
|
|
343
|
+
return `${tableAlias}.${wrap(colName)} ${sort}`;
|
|
322
344
|
} else {
|
|
323
345
|
// Need to force NULL positioning with CASE
|
|
324
|
-
return `CASE WHEN ${tableAlias}.${colName} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${colName} ${sort}`;
|
|
346
|
+
return `CASE WHEN ${tableAlias}.${wrap(colName)} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${wrap(colName)} ${sort}`;
|
|
325
347
|
}
|
|
326
348
|
}
|
|
327
349
|
} else {
|
|
@@ -333,13 +355,7 @@ export const buildOrderByClauseFp =
|
|
|
333
355
|
) ${sort}`;
|
|
334
356
|
}
|
|
335
357
|
|
|
336
|
-
|
|
337
|
-
return `${tableAlias}."${colName}" ${sort}`;
|
|
338
|
-
} else if (dbType === "mysql") {
|
|
339
|
-
return `${tableAlias}.${colName} ${sort}`;
|
|
340
|
-
} else {
|
|
341
|
-
return `${tableAlias}.${colName} ${sort}`;
|
|
342
|
-
}
|
|
358
|
+
return `${tableAlias}.${wrap(colName)} ${sort}`;
|
|
343
359
|
}
|
|
344
360
|
})
|
|
345
361
|
.join(", ");
|
|
@@ -463,7 +479,6 @@ export const processFieldSelectionsFp =
|
|
|
463
479
|
level: number,
|
|
464
480
|
buildSubquery: (sel: SelectionAnalysis, level: number) => string,
|
|
465
481
|
selectBuilder: (entry: [name: string, selector: string]) => string,
|
|
466
|
-
quoted = false,
|
|
467
482
|
): string => {
|
|
468
483
|
const selectClauses: [string, string][] = [];
|
|
469
484
|
const subQueries: [string, string][] = [];
|
|
@@ -487,7 +502,7 @@ export const processFieldSelectionsFp =
|
|
|
487
502
|
selectClauses.push([`${colName}`, `(${vc.expression})`]);
|
|
488
503
|
}
|
|
489
504
|
} else {
|
|
490
|
-
let querySelector = `${tableAlias}.${
|
|
505
|
+
let querySelector = `${tableAlias}.${wrapIdentifierFp(dbType)(sel.name)}`;
|
|
491
506
|
|
|
492
507
|
// Apply all directives using the handler registry
|
|
493
508
|
querySelector = applyDirectives(
|
|
@@ -141,6 +141,8 @@ export const callStoredProcedure = async (
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// Unquoted on purpose: tedious sends this as the TDS RPC procedure name,
|
|
145
|
+
// never as SQL text, so reserved words need no delimiting here.
|
|
144
146
|
const data = await request.execute(sp.dottedName);
|
|
145
147
|
|
|
146
148
|
return data?.recordset ?? false;
|
|
@@ -16,12 +16,16 @@ import {
|
|
|
16
16
|
isAggregationField,
|
|
17
17
|
isSingleQuery,
|
|
18
18
|
processFieldSelectionsMSSQL,
|
|
19
|
+
wrapIdentifierMSSQL,
|
|
19
20
|
} from "../../../common";
|
|
20
21
|
|
|
22
|
+
// FOR JSON PATH uses the unbracketed alias as the JSON key, so bracket-quoting
|
|
23
|
+
// (required for reserved words like PRINT) never leaks into results.
|
|
24
|
+
|
|
21
25
|
// Generate CTE for aggregations
|
|
22
26
|
const buildAggregationCTE = (
|
|
23
27
|
groupByInfo: GroupByInfo,
|
|
24
|
-
|
|
28
|
+
dottedQuotedName: string,
|
|
25
29
|
tableAlias: string,
|
|
26
30
|
whereClause: string,
|
|
27
31
|
): string => {
|
|
@@ -31,7 +35,7 @@ const buildAggregationCTE = (
|
|
|
31
35
|
|
|
32
36
|
// Add group by fields
|
|
33
37
|
groupByFields.forEach((field) => {
|
|
34
|
-
selectClauses.push(`${tableAlias}.${field}`);
|
|
38
|
+
selectClauses.push(`${tableAlias}.${wrapIdentifierMSSQL(field)}`);
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
// Add aggregations
|
|
@@ -40,16 +44,18 @@ const buildAggregationCTE = (
|
|
|
40
44
|
selectClauses.push(`COUNT(*) AS ${agg.alias}`);
|
|
41
45
|
} else {
|
|
42
46
|
const func = agg.name.toUpperCase();
|
|
43
|
-
selectClauses.push(
|
|
47
|
+
selectClauses.push(
|
|
48
|
+
`${func}(${tableAlias}.${wrapIdentifierMSSQL(agg.fieldName)}) AS ${agg.alias}`,
|
|
49
|
+
);
|
|
44
50
|
}
|
|
45
51
|
});
|
|
46
52
|
|
|
47
|
-
const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${field}`).join(", ")}`;
|
|
53
|
+
const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMSSQL(field)}`).join(", ")}`;
|
|
48
54
|
|
|
49
55
|
return `${cteAlias} AS (
|
|
50
56
|
SELECT
|
|
51
57
|
${selectClauses.join(",\n ")}
|
|
52
|
-
FROM ${
|
|
58
|
+
FROM ${dottedQuotedName} ${tableAlias}
|
|
53
59
|
${whereClause}
|
|
54
60
|
${groupByClause}
|
|
55
61
|
)`;
|
|
@@ -60,7 +66,7 @@ const buildGroupedQuery = (
|
|
|
60
66
|
entities: MergedEntities,
|
|
61
67
|
field: SelectionAnalysis,
|
|
62
68
|
groupByInfo: GroupByInfo,
|
|
63
|
-
|
|
69
|
+
dottedQuotedName: string,
|
|
64
70
|
tableAlias: string,
|
|
65
71
|
whereClause: string,
|
|
66
72
|
): string => {
|
|
@@ -72,21 +78,24 @@ const buildGroupedQuery = (
|
|
|
72
78
|
if (hasKey) {
|
|
73
79
|
// Add key object with group by fields
|
|
74
80
|
const keyFields = keys
|
|
75
|
-
.map(
|
|
81
|
+
.map(
|
|
82
|
+
(field) =>
|
|
83
|
+
`${cteAlias}.${wrapIdentifierMSSQL(field.name)} AS ${wrapIdentifierMSSQL(field.alias || field.name)}`,
|
|
84
|
+
)
|
|
76
85
|
.join(", ");
|
|
77
86
|
|
|
78
87
|
selectClauses.push(
|
|
79
|
-
`JSON_QUERY((SELECT ${keyFields} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS
|
|
88
|
+
`JSON_QUERY((SELECT ${keyFields} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS ${wrapIdentifierMSSQL(keyResolved)}`,
|
|
80
89
|
);
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
// Add aggregation results
|
|
84
93
|
aggregations.forEach((agg) => {
|
|
85
94
|
if (agg.name === "count") {
|
|
86
|
-
selectClauses.push(`${cteAlias}.${agg.alias} AS ${agg.nameResolved}`);
|
|
95
|
+
selectClauses.push(`${cteAlias}.${agg.alias} AS ${wrapIdentifierMSSQL(agg.nameResolved)}`);
|
|
87
96
|
} else {
|
|
88
97
|
selectClauses.push(
|
|
89
|
-
`JSON_QUERY((SELECT ${cteAlias}.${agg.alias} AS ${agg.fieldAlias} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS ${agg.nameResolved}`,
|
|
98
|
+
`JSON_QUERY((SELECT ${cteAlias}.${agg.alias} AS ${wrapIdentifierMSSQL(agg.fieldAlias)} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS ${wrapIdentifierMSSQL(agg.nameResolved)}`,
|
|
90
99
|
);
|
|
91
100
|
}
|
|
92
101
|
});
|
|
@@ -98,20 +107,24 @@ const buildGroupedQuery = (
|
|
|
98
107
|
if (itemsSelection?.selections) {
|
|
99
108
|
const itemFields = itemsSelection.selections
|
|
100
109
|
.filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
|
|
101
|
-
.map(
|
|
110
|
+
.map(
|
|
111
|
+
(sel) =>
|
|
112
|
+
`${tableAlias}.${wrapIdentifierMSSQL(sel.name)} AS ${wrapIdentifierMSSQL(sel.alias || sel.name)}`,
|
|
113
|
+
)
|
|
102
114
|
.join(", ");
|
|
103
115
|
|
|
104
116
|
if (itemFields) {
|
|
105
117
|
const joinConditions = groupByFields.map(
|
|
106
|
-
(field) =>
|
|
118
|
+
(field) =>
|
|
119
|
+
`${tableAlias}.${wrapIdentifierMSSQL(field)} = ${cteAlias}.${wrapIdentifierMSSQL(field)}`,
|
|
107
120
|
);
|
|
108
121
|
|
|
109
122
|
selectClauses.push(`JSON_QUERY(ISNULL((
|
|
110
123
|
SELECT ${itemFields}
|
|
111
|
-
FROM ${
|
|
124
|
+
FROM ${dottedQuotedName} ${tableAlias}
|
|
112
125
|
${whereClause ? [whereClause, ...joinConditions].join(" AND ") : `WHERE ${joinConditions.join(" AND ")}`}
|
|
113
126
|
FOR JSON PATH, INCLUDE_NULL_VALUES
|
|
114
|
-
), '[]')) AS ${itemsSelection.alias || itemsSelection.name}`);
|
|
127
|
+
), '[]')) AS ${wrapIdentifierMSSQL(itemsSelection.alias || itemsSelection.name)}`);
|
|
115
128
|
}
|
|
116
129
|
}
|
|
117
130
|
}
|
|
@@ -168,7 +181,7 @@ export const generateSQL = (
|
|
|
168
181
|
|
|
169
182
|
if (groupByInfo) {
|
|
170
183
|
// This field requires a CTE
|
|
171
|
-
const {
|
|
184
|
+
const { dottedQuotedName } = entities.queriesMap[field.name]!;
|
|
172
185
|
|
|
173
186
|
const whereClause = buildWhereClauseMSSQL(
|
|
174
187
|
entities,
|
|
@@ -182,7 +195,7 @@ export const generateSQL = (
|
|
|
182
195
|
{},
|
|
183
196
|
);
|
|
184
197
|
|
|
185
|
-
const cte = buildAggregationCTE(groupByInfo,
|
|
198
|
+
const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
|
|
186
199
|
ctes.push(cte);
|
|
187
200
|
}
|
|
188
201
|
|
|
@@ -197,7 +210,7 @@ export const generateSQL = (
|
|
|
197
210
|
{},
|
|
198
211
|
);
|
|
199
212
|
|
|
200
|
-
fieldQueries.push(`(${fieldSQL}) as ${field.alias || field.name}`);
|
|
213
|
+
fieldQueries.push(`(${fieldSQL}) as ${wrapIdentifierMSSQL(field.alias || field.name)}`);
|
|
201
214
|
});
|
|
202
215
|
|
|
203
216
|
const cteClause = ctes.length > 0 ? `WITH\n${ctes.join(",\n")}\n` : "";
|
|
@@ -229,7 +242,7 @@ export const buildSQLForField = (
|
|
|
229
242
|
throw new Error(`Table not found for field: ${field.name}`);
|
|
230
243
|
}
|
|
231
244
|
|
|
232
|
-
const {
|
|
245
|
+
const { dottedQuotedName, resolverName } = foundTable;
|
|
233
246
|
|
|
234
247
|
aliasMap[tableAlias] = resolverName;
|
|
235
248
|
|
|
@@ -254,7 +267,7 @@ export const buildSQLForField = (
|
|
|
254
267
|
entities,
|
|
255
268
|
field,
|
|
256
269
|
groupByInfo,
|
|
257
|
-
|
|
270
|
+
dottedQuotedName,
|
|
258
271
|
tableAlias,
|
|
259
272
|
whereClause,
|
|
260
273
|
);
|
|
@@ -282,10 +295,10 @@ export const buildSQLForField = (
|
|
|
282
295
|
level,
|
|
283
296
|
aliasMap,
|
|
284
297
|
),
|
|
285
|
-
([name, selector]) => `${selector} AS ${name}`,
|
|
298
|
+
([name, selector]) => `${selector} AS ${wrapIdentifierMSSQL(name)}`,
|
|
286
299
|
);
|
|
287
300
|
|
|
288
|
-
const fromClause = `FROM ${
|
|
301
|
+
const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
|
|
289
302
|
|
|
290
303
|
const orderByClause = buildOrderByClauseMSSQL(entities, field, tableAlias);
|
|
291
304
|
const paginationClause = buildPaginationClauseMSSQL(field, variablesDefinition);
|
|
@@ -85,7 +85,7 @@ export const callStoredProcedure = async (
|
|
|
85
85
|
) => {
|
|
86
86
|
try {
|
|
87
87
|
const data = await executeQuery(
|
|
88
|
-
`CALL ${sp.
|
|
88
|
+
`CALL ${sp.dottedQuotedName}(${Object.keys(variables)
|
|
89
89
|
.map((_, i) => `$${i + 1}`)
|
|
90
90
|
.join(", ")});`,
|
|
91
91
|
sp.db!,
|
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
isAggregationField,
|
|
17
17
|
isSingleQuery,
|
|
18
18
|
processFieldSelectionsMySQL,
|
|
19
|
+
wrapIdentifierMySQL,
|
|
19
20
|
} from "../../../common";
|
|
20
21
|
|
|
21
22
|
// Generate CTE for aggregations
|
|
22
23
|
const buildAggregationCTE = (
|
|
23
24
|
groupByInfo: GroupByInfo,
|
|
24
|
-
|
|
25
|
+
dottedQuotedName: string,
|
|
25
26
|
tableAlias: string,
|
|
26
27
|
whereClause: string,
|
|
27
28
|
): string => {
|
|
@@ -31,7 +32,7 @@ const buildAggregationCTE = (
|
|
|
31
32
|
|
|
32
33
|
// Add group by fields
|
|
33
34
|
groupByFields.forEach((field) => {
|
|
34
|
-
selectClauses.push(`${tableAlias}.${field}`);
|
|
35
|
+
selectClauses.push(`${tableAlias}.${wrapIdentifierMySQL(field)}`);
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
// Add aggregations
|
|
@@ -40,16 +41,18 @@ const buildAggregationCTE = (
|
|
|
40
41
|
selectClauses.push(`COUNT(*) AS ${agg.alias}`);
|
|
41
42
|
} else {
|
|
42
43
|
const func = agg.name.toUpperCase();
|
|
43
|
-
selectClauses.push(
|
|
44
|
+
selectClauses.push(
|
|
45
|
+
`${func}(${tableAlias}.${wrapIdentifierMySQL(agg.fieldName)}) AS ${agg.alias}`,
|
|
46
|
+
);
|
|
44
47
|
}
|
|
45
48
|
});
|
|
46
49
|
|
|
47
|
-
const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${field}`).join(", ")}`;
|
|
50
|
+
const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMySQL(field)}`).join(", ")}`;
|
|
48
51
|
|
|
49
52
|
return `${cteAlias} AS (
|
|
50
53
|
SELECT
|
|
51
54
|
${selectClauses.join(",\n ")}
|
|
52
|
-
FROM ${
|
|
55
|
+
FROM ${dottedQuotedName} ${tableAlias}
|
|
53
56
|
${whereClause}
|
|
54
57
|
${groupByClause}
|
|
55
58
|
)`;
|
|
@@ -60,7 +63,7 @@ const buildGroupedQuery = (
|
|
|
60
63
|
entities: MergedEntities,
|
|
61
64
|
field: SelectionAnalysis,
|
|
62
65
|
groupByInfo: GroupByInfo,
|
|
63
|
-
|
|
66
|
+
dottedQuotedName: string,
|
|
64
67
|
tableAlias: string,
|
|
65
68
|
whereClause: string,
|
|
66
69
|
): string => {
|
|
@@ -72,7 +75,7 @@ const buildGroupedQuery = (
|
|
|
72
75
|
if (hasKey) {
|
|
73
76
|
// Add key object with group by fields
|
|
74
77
|
const keyFields = keys
|
|
75
|
-
.map((key) => `'${key.alias || key.name}', ${cteAlias}.${key.name}`)
|
|
78
|
+
.map((key) => `'${key.alias || key.name}', ${cteAlias}.${wrapIdentifierMySQL(key.name)}`)
|
|
76
79
|
.join(", ");
|
|
77
80
|
|
|
78
81
|
selectClauses.push(`'${keyResolved}', JSON_OBJECT(${keyFields})`);
|
|
@@ -96,12 +99,13 @@ const buildGroupedQuery = (
|
|
|
96
99
|
if (itemsSelection?.selections) {
|
|
97
100
|
const itemFields = itemsSelection.selections
|
|
98
101
|
.filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
|
|
99
|
-
.map((sel) => `'${sel.alias || sel.name}', ${tableAlias}.${sel.name}`)
|
|
102
|
+
.map((sel) => `'${sel.alias || sel.name}', ${tableAlias}.${wrapIdentifierMySQL(sel.name)}`)
|
|
100
103
|
.join(", ");
|
|
101
104
|
|
|
102
105
|
if (itemFields) {
|
|
103
106
|
const joinConditions = groupByFields.map(
|
|
104
|
-
(field) =>
|
|
107
|
+
(field) =>
|
|
108
|
+
`${tableAlias}.${wrapIdentifierMySQL(field)} = ${cteAlias}.${wrapIdentifierMySQL(field)}`,
|
|
105
109
|
);
|
|
106
110
|
|
|
107
111
|
const whereConditions = whereClause
|
|
@@ -110,7 +114,7 @@ const buildGroupedQuery = (
|
|
|
110
114
|
|
|
111
115
|
selectClauses.push(`'${itemsSelection.alias || itemsSelection.name}', COALESCE((
|
|
112
116
|
SELECT JSON_ARRAYAGG(JSON_OBJECT(${itemFields}))
|
|
113
|
-
FROM ${
|
|
117
|
+
FROM ${dottedQuotedName} ${tableAlias}
|
|
114
118
|
${whereConditions}
|
|
115
119
|
), JSON_ARRAY())`);
|
|
116
120
|
}
|
|
@@ -157,7 +161,7 @@ export const generateSQL = (
|
|
|
157
161
|
|
|
158
162
|
if (groupByInfo) {
|
|
159
163
|
// This field requires a CTE
|
|
160
|
-
const {
|
|
164
|
+
const { dottedQuotedName } = entities.queriesMap[field.name]!;
|
|
161
165
|
|
|
162
166
|
const whereClause = buildWhereClauseMySQL(
|
|
163
167
|
entities,
|
|
@@ -169,10 +173,9 @@ export const generateSQL = (
|
|
|
169
173
|
null,
|
|
170
174
|
index + 1,
|
|
171
175
|
{},
|
|
172
|
-
false,
|
|
173
176
|
);
|
|
174
177
|
|
|
175
|
-
const cte = buildAggregationCTE(groupByInfo,
|
|
178
|
+
const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
|
|
176
179
|
ctes.push(cte);
|
|
177
180
|
}
|
|
178
181
|
|
|
@@ -218,7 +221,7 @@ export const buildSQLForField = (
|
|
|
218
221
|
throw new Error(`Table not found for field: ${field.name}`);
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
const {
|
|
224
|
+
const { dottedQuotedName, resolverName } = foundTable;
|
|
222
225
|
|
|
223
226
|
aliasMap[tableAlias] = resolverName;
|
|
224
227
|
|
|
@@ -232,7 +235,6 @@ export const buildSQLForField = (
|
|
|
232
235
|
parentTableAlias,
|
|
233
236
|
level,
|
|
234
237
|
aliasMap,
|
|
235
|
-
false,
|
|
236
238
|
);
|
|
237
239
|
|
|
238
240
|
// Check if this is a GROUP BY query
|
|
@@ -244,7 +246,7 @@ export const buildSQLForField = (
|
|
|
244
246
|
entities,
|
|
245
247
|
field,
|
|
246
248
|
groupByInfo,
|
|
247
|
-
|
|
249
|
+
dottedQuotedName,
|
|
248
250
|
tableAlias,
|
|
249
251
|
whereClause,
|
|
250
252
|
);
|
|
@@ -273,10 +275,9 @@ export const buildSQLForField = (
|
|
|
273
275
|
aliasMap,
|
|
274
276
|
),
|
|
275
277
|
([name, selector]) => `'${name}', ${selector}`,
|
|
276
|
-
false,
|
|
277
278
|
);
|
|
278
279
|
|
|
279
|
-
const fromClause = `FROM ${
|
|
280
|
+
const fromClause = `FROM ${dottedQuotedName} ${tableAlias}${withoutArrayWrapper ? " LIMIT 1" : ""}`;
|
|
280
281
|
|
|
281
282
|
const orderByClause = buildOrderByClauseMySQL(entities, field, tableAlias);
|
|
282
283
|
const paginationClause = buildPaginationClauseMySQL(field, variablesDefinition);
|
|
@@ -84,7 +84,7 @@ export const callStoredProcedure = async (
|
|
|
84
84
|
) => {
|
|
85
85
|
try {
|
|
86
86
|
const data = await executeQuery(
|
|
87
|
-
`SELECT * FROM ${sp.
|
|
87
|
+
`SELECT * FROM ${sp.dottedQuotedName}(${Object.keys(variables)
|
|
88
88
|
.map((_, i) => `$${i + 1}`)
|
|
89
89
|
.join(", ")});`,
|
|
90
90
|
sp.db!,
|
|
@@ -169,7 +169,6 @@ export const generateSQL = (
|
|
|
169
169
|
null,
|
|
170
170
|
index + 1,
|
|
171
171
|
{},
|
|
172
|
-
true,
|
|
173
172
|
);
|
|
174
173
|
|
|
175
174
|
const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
|
|
@@ -232,7 +231,6 @@ export const buildSQLForField = (
|
|
|
232
231
|
parentTableAlias,
|
|
233
232
|
level,
|
|
234
233
|
aliasMap,
|
|
235
|
-
true,
|
|
236
234
|
);
|
|
237
235
|
|
|
238
236
|
// Check if this is a GROUP BY query
|
|
@@ -273,7 +271,6 @@ export const buildSQLForField = (
|
|
|
273
271
|
aliasMap,
|
|
274
272
|
),
|
|
275
273
|
([name, selector]) => `'${name}', ${selector}`,
|
|
276
|
-
true,
|
|
277
274
|
);
|
|
278
275
|
|
|
279
276
|
const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import type { DatabaseType } from "../../../types/configuration";
|
|
2
2
|
import type { TableResolver } from "../../../types/db";
|
|
3
3
|
|
|
4
|
+
import { qualifiedNameFp, wrapIdentifierFp } from "../../common";
|
|
5
|
+
|
|
4
6
|
export const generateInsertSQL =
|
|
5
7
|
(toDb: DatabaseType) =>
|
|
6
8
|
// oxlint-disable-next-line typescript/no-explicit-any
|
|
7
9
|
(table: TableResolver, data: Record<string, any>[]) => {
|
|
8
|
-
const columnNames = table.columns.map((column) =>
|
|
9
|
-
if (toDb === "mssql") {
|
|
10
|
-
return column.name;
|
|
11
|
-
} else if (toDb === "mysql") {
|
|
12
|
-
return `\`${column.name}\``;
|
|
13
|
-
} else {
|
|
14
|
-
return `"${column.name}"`;
|
|
15
|
-
}
|
|
16
|
-
});
|
|
10
|
+
const columnNames = table.columns.map((column) => wrapIdentifierFp(toDb)(column.name));
|
|
17
11
|
|
|
18
12
|
const valueRows = data.map((row) => {
|
|
19
13
|
const values = table.columns.map((column) => {
|
|
@@ -26,18 +20,9 @@ export const generateInsertSQL =
|
|
|
26
20
|
return `(${values.join(",")})`;
|
|
27
21
|
});
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return `INSERT INTO ${table?.dottedName} (${columnNames.join(",")}) VALUES\n${valueRows.join(",\n")}`;
|
|
33
|
-
} else if (toDb === "mysql") {
|
|
34
|
-
// Generate backtick version of dotted name
|
|
35
|
-
const parts = table.dottedName.split(".");
|
|
36
|
-
const dottedBacktickName = parts.map((p) => `\`${p}\``).join(".");
|
|
37
|
-
return `INSERT INTO ${dottedBacktickName} (${columnNames.join(",")}) VALUES\n${valueRows.join(",\n")}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return "";
|
|
23
|
+
// Quoted for the target engine, not the source: table.dottedQuotedName
|
|
24
|
+
// carries the source dialect's delimiters and the two differ when converting.
|
|
25
|
+
return `INSERT INTO ${qualifiedNameFp(toDb)(table)} (${columnNames.join(",")}) VALUES\n${valueRows.join(",\n")}`;
|
|
41
26
|
};
|
|
42
27
|
|
|
43
28
|
export type GenerateInsertSQL = ReturnType<typeof generateInsertSQL>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Database, Relationships } from "../../types/configuration";
|
|
2
2
|
import type { RelationshipResolver, StoredProcedure, Table, Tables } from "../../types/db";
|
|
3
3
|
|
|
4
|
+
import { qualifiedNameFp } from "../common";
|
|
4
5
|
import { genResolverName } from "./genResolverName";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -103,6 +104,7 @@ export const buildTableResolver = (tables: Tables, table: Table, db: Database) =
|
|
|
103
104
|
return {
|
|
104
105
|
...table,
|
|
105
106
|
db,
|
|
107
|
+
dottedQuotedName: qualifiedNameFp(db.type)(table),
|
|
106
108
|
internalName,
|
|
107
109
|
resolverName: internalName,
|
|
108
110
|
relationships,
|
|
@@ -124,5 +126,6 @@ export const buildProcedureResolver = (sp: StoredProcedure, db: Database) => {
|
|
|
124
126
|
internalName,
|
|
125
127
|
resolverName,
|
|
126
128
|
dottedName,
|
|
129
|
+
dottedQuotedName: qualifiedNameFp(db.type)(sp),
|
|
127
130
|
};
|
|
128
131
|
};
|
package/src/index.ts
CHANGED