@graphoria/server 0.1.4 → 0.2.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/dist/src/analyzeQuery/resolveVariables.d.ts.map +1 -1
- package/dist/src/config/helpers/operationHelper.d.ts +60 -6
- package/dist/src/config/helpers/operationHelper.d.ts.map +1 -1
- package/dist/src/config/helpers/operationHelper.test.d.ts +2 -0
- package/dist/src/config/helpers/operationHelper.test.d.ts.map +1 -0
- package/dist/src/config/index.d.ts +65 -60
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/types/ai.d.ts +9 -3
- package/dist/src/config/types/ai.d.ts.map +1 -1
- package/dist/src/config/types/auth.d.ts +55 -15
- package/dist/src/config/types/auth.d.ts.map +1 -1
- package/dist/src/config/types/cron.d.ts +96 -2
- package/dist/src/config/types/cron.d.ts.map +1 -1
- package/dist/src/config/types/db.d.ts +186 -24
- package/dist/src/config/types/db.d.ts.map +1 -1
- package/dist/src/config/types/index.d.ts +3 -3
- package/dist/src/config/types/index.d.ts.map +1 -1
- package/dist/src/config/types/operation.d.ts +49 -17
- package/dist/src/config/types/operation.d.ts.map +1 -1
- package/dist/src/config/types/queue.d.ts +59 -12
- package/dist/src/config/types/queue.d.ts.map +1 -1
- package/dist/src/config/types/remote-rest.d.ts +10 -1
- package/dist/src/config/types/remote-rest.d.ts.map +1 -1
- package/dist/src/config/types/remote-schema.d.ts +13 -3
- package/dist/src/config/types/remote-schema.d.ts.map +1 -1
- package/dist/src/config/types/virtual-columns.d.ts +1 -1
- package/dist/src/config/types/virtual-columns.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/index.d.ts +52 -4
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +26 -2
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts +2 -0
- package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts.map +1 -0
- package/dist/src/configuration/getSchemas/type-definition-generator/index.d.ts.map +1 -1
- package/dist/src/configuration/index.d.ts +40 -4
- package/dist/src/configuration/index.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/databases/common.d.ts +3 -1
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/core/query-builder.d.ts +7 -1
- package/dist/src/databases/core/query-builder.d.ts.map +1 -1
- package/dist/src/databases/directives.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/getStructure.d.ts +7 -0
- 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/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getStructure.d.ts +7 -0
- package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/placeholders.d.ts +22 -0
- package/dist/src/databases/engines/mysql/placeholders.d.ts.map +1 -0
- package/dist/src/databases/engines/mysql/placeholders.test.d.ts +2 -0
- package/dist/src/databases/engines/mysql/placeholders.test.d.ts.map +1 -0
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getStructure.d.ts +7 -0
- 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 +14 -1
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/metadata/structure.d.ts +7 -0
- package/dist/src/databases/metadata/structure.d.ts.map +1 -1
- package/dist/src/databases/sqlTypeUtils.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +39 -2
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/singletons/env.d.ts +21 -3
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/types/configuration.d.ts +1 -1
- package/dist/src/types/configuration.d.ts.map +1 -1
- package/dist/src/types/env.d.ts +127 -51
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/types/resolver.d.ts +12 -2
- package/dist/src/types/resolver.d.ts.map +1 -1
- package/dist/src/types/zod/auth.d.ts +6 -6
- package/dist/src/types/zod/configuration.d.ts +85 -45
- package/dist/src/types/zod/configuration.d.ts.map +1 -1
- package/dist/src/types/zod/cron.d.ts +1 -1
- package/dist/src/types/zod/cron.d.ts.map +1 -1
- package/dist/src/types/zod/db.d.ts +167 -8
- package/dist/src/types/zod/db.d.ts.map +1 -1
- package/dist/src/types/zod/queue.d.ts +26 -26
- package/dist/src/types/zod/strict-keys.test.d.ts +2 -0
- package/dist/src/types/zod/strict-keys.test.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/analyzeQuery/resolveVariables.ts +37 -7
- package/src/config/helpers/operationHelper.ts +146 -5
- package/src/config/index.ts +139 -62
- package/src/config/types/ai.ts +2 -2
- package/src/config/types/auth.ts +37 -6
- package/src/config/types/cron.ts +1 -1
- package/src/config/types/db.ts +56 -9
- package/src/config/types/index.ts +5 -0
- package/src/config/types/operation.ts +49 -9
- package/src/config/types/queue.ts +8 -8
- package/src/config/types/remote-rest.ts +1 -1
- package/src/config/types/remote-schema.ts +2 -2
- package/src/config/types/virtual-columns.ts +1 -1
- package/src/configuration/getSchemas/mergeEntities/index.ts +6 -0
- package/src/configuration/getSchemas/type-definition-generator/index.ts +13 -4
- package/src/configuration/rest/generateOpenAPI.ts +5 -9
- package/src/configuration/rest/handleRESTRequestFactory.ts +40 -14
- package/src/databases/common.ts +279 -83
- package/src/databases/directives.ts +26 -8
- package/src/databases/engines/mssql/connection.ts +1 -1
- package/src/databases/engines/mssql/getStructure.ts +1 -0
- package/src/databases/engines/mssql/query/index.ts +29 -11
- package/src/databases/engines/mysql/auth.ts +2 -2
- package/src/databases/engines/mysql/connection.ts +5 -4
- package/src/databases/engines/mysql/getStructure.ts +61 -29
- package/src/databases/engines/mysql/placeholders.ts +84 -0
- package/src/databases/engines/mysql/query/index.ts +49 -4
- package/src/databases/engines/postgresql/getStructure.ts +1 -0
- package/src/databases/engines/postgresql/query/index.ts +40 -2
- package/src/databases/high-level-operations.ts +25 -0
- package/src/databases/sqlTypeUtils.ts +3 -1
- package/src/subscriptions/strategies/index.ts +1 -1
- package/src/subscriptions/strategies/queue.ts +1 -1
- package/src/types/configuration.ts +7 -1
- package/src/types/resolver.ts +17 -1
- package/src/types/zod/configuration.ts +1 -1
- package/src/types/zod/db.ts +4 -0
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
wrapIdentifierMSSQL,
|
|
20
20
|
} from "../../../common";
|
|
21
21
|
|
|
22
|
+
import { applyDirectives } from "../../../directives";
|
|
23
|
+
|
|
22
24
|
// FOR JSON PATH uses the unbracketed alias as the JSON key, so bracket-quoting
|
|
23
25
|
// (required for reserved words like PRINT) never leaks into results.
|
|
24
26
|
|
|
@@ -44,9 +46,13 @@ const buildAggregationCTE = (
|
|
|
44
46
|
selectClauses.push(`COUNT(*) AS ${agg.alias}`);
|
|
45
47
|
} else {
|
|
46
48
|
const func = agg.name.toUpperCase();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// SQL Server's AVG over an integer column is integer division — AVG of
|
|
50
|
+
// 1, 5, 5, 2 is 3, not 3.25 — so the average has to be taken on a float.
|
|
51
|
+
const operand =
|
|
52
|
+
agg.name === "avg"
|
|
53
|
+
? `CAST(${tableAlias}.${wrapIdentifierMSSQL(agg.fieldName)} AS FLOAT)`
|
|
54
|
+
: `${tableAlias}.${wrapIdentifierMSSQL(agg.fieldName)}`;
|
|
55
|
+
selectClauses.push(`${func}(${operand}) AS ${agg.alias}`);
|
|
50
56
|
}
|
|
51
57
|
});
|
|
52
58
|
|
|
@@ -64,6 +70,7 @@ const buildAggregationCTE = (
|
|
|
64
70
|
// Build the main query for grouped results
|
|
65
71
|
const buildGroupedQuery = (
|
|
66
72
|
entities: MergedEntities,
|
|
73
|
+
variablesDefinition: VariableDefinition[],
|
|
67
74
|
field: SelectionAnalysis,
|
|
68
75
|
groupByInfo: GroupByInfo,
|
|
69
76
|
dottedQuotedName: string,
|
|
@@ -78,10 +85,15 @@ const buildGroupedQuery = (
|
|
|
78
85
|
if (hasKey) {
|
|
79
86
|
// Add key object with group by fields
|
|
80
87
|
const keyFields = keys
|
|
81
|
-
.map(
|
|
82
|
-
(
|
|
83
|
-
`${cteAlias}.${wrapIdentifierMSSQL(field.name)}
|
|
84
|
-
|
|
88
|
+
.map((field) => {
|
|
89
|
+
const selector = applyDirectives(
|
|
90
|
+
`${cteAlias}.${wrapIdentifierMSSQL(field.name)}`,
|
|
91
|
+
field.directives,
|
|
92
|
+
"mssql",
|
|
93
|
+
variablesDefinition,
|
|
94
|
+
);
|
|
95
|
+
return `${selector} AS ${wrapIdentifierMSSQL(field.alias || field.name)}`;
|
|
96
|
+
})
|
|
85
97
|
.join(", ");
|
|
86
98
|
|
|
87
99
|
selectClauses.push(
|
|
@@ -107,10 +119,15 @@ const buildGroupedQuery = (
|
|
|
107
119
|
if (itemsSelection?.selections) {
|
|
108
120
|
const itemFields = itemsSelection.selections
|
|
109
121
|
.filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
|
|
110
|
-
.map(
|
|
111
|
-
(
|
|
112
|
-
`${tableAlias}.${wrapIdentifierMSSQL(sel.name)}
|
|
113
|
-
|
|
122
|
+
.map((sel) => {
|
|
123
|
+
const selector = applyDirectives(
|
|
124
|
+
`${tableAlias}.${wrapIdentifierMSSQL(sel.name)}`,
|
|
125
|
+
sel.directives,
|
|
126
|
+
"mssql",
|
|
127
|
+
variablesDefinition,
|
|
128
|
+
);
|
|
129
|
+
return `${selector} AS ${wrapIdentifierMSSQL(sel.alias || sel.name)}`;
|
|
130
|
+
})
|
|
114
131
|
.join(", ");
|
|
115
132
|
|
|
116
133
|
if (itemFields) {
|
|
@@ -265,6 +282,7 @@ export const buildSQLForField = (
|
|
|
265
282
|
// Handle GROUP BY aggregation query
|
|
266
283
|
const mainQuery = buildGroupedQuery(
|
|
267
284
|
entities,
|
|
285
|
+
variablesDefinition,
|
|
268
286
|
field,
|
|
269
287
|
groupByInfo,
|
|
270
288
|
dottedQuotedName,
|
|
@@ -41,7 +41,7 @@ export const checkUserCredentials = async (
|
|
|
41
41
|
const schema = assertSafeIdentifier(auth.schema!, "schema");
|
|
42
42
|
|
|
43
43
|
const result = await pool.unsafe<UserRecord[]>(
|
|
44
|
-
`SELECT * FROM \`${schema}\`.\`user\` WHERE username =
|
|
44
|
+
`SELECT * FROM \`${schema}\`.\`user\` WHERE username = ? AND is_active = TRUE`,
|
|
45
45
|
[username],
|
|
46
46
|
);
|
|
47
47
|
|
|
@@ -94,7 +94,7 @@ export const insertAuthUser = async (
|
|
|
94
94
|
|
|
95
95
|
await pool.unsafe(
|
|
96
96
|
`INSERT INTO \`${schema}\`.\`user\` (username, password, role, is_active, claims)
|
|
97
|
-
VALUES (
|
|
97
|
+
VALUES (?, ?, ?, TRUE, ?)`,
|
|
98
98
|
[input.username, hashed, input.role, JSON.stringify(input.claims ?? {})],
|
|
99
99
|
);
|
|
100
100
|
};
|
|
@@ -6,6 +6,7 @@ import type { Database } from "../../../types/configuration";
|
|
|
6
6
|
import type { ProcedureResolver } from "../../../types/db";
|
|
7
7
|
|
|
8
8
|
import { databasesConnections } from "../../../singletons/databases";
|
|
9
|
+
import { toMySQLPlaceholders } from "./placeholders";
|
|
9
10
|
|
|
10
11
|
export const getPool = async (db: Database) => {
|
|
11
12
|
const opts = db.connectionOptions as BunSQLConnectionOptions | undefined;
|
|
@@ -42,10 +43,10 @@ export const executeQueryFactory =
|
|
|
42
43
|
) => {
|
|
43
44
|
const pool = singleQuery ? await getPool(db) : await getPoolSingleton(db);
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
46
|
+
// Bun's MySQL adapter binds `?` positionally; the builders emit `$n`.
|
|
47
|
+
const bound = toMySQLPlaceholders(query, variablesDefinition, values);
|
|
48
|
+
|
|
49
|
+
const result = await pool.unsafe<T>(bound.query, bound.params);
|
|
49
50
|
|
|
50
51
|
if (singleQuery) {
|
|
51
52
|
await pool.close();
|
|
@@ -3,8 +3,23 @@ import type { Database } from "../../../types/configuration";
|
|
|
3
3
|
import { DatabaseStructureZod } from "../../../types/zod/db";
|
|
4
4
|
import { executeQueryJSONSingle } from "./connection";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Ordering carried out of SQL and restored here: JSON_ARRAYAGG accepts no
|
|
8
|
+
* ORDER BY. GROUP_CONCAT does, but silently truncates at group_concat_max_len
|
|
9
|
+
* (1024 bytes by default), which any table past ~10 columns exceeds — the JSON
|
|
10
|
+
* is then cut mid-object and CAST(... AS JSON) fails.
|
|
11
|
+
*/
|
|
12
|
+
type Ordered = { ordinalPosition?: number };
|
|
13
|
+
type RawForeignKey = { fkName?: string; columns: Ordered[] };
|
|
14
|
+
type RawStructure = {
|
|
15
|
+
tables?: { columns?: Ordered[]; foreignKeys?: RawForeignKey[] }[];
|
|
16
|
+
storedProcedures?: { parameters?: Ordered[] }[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const byOrdinal = (a: Ordered, b: Ordered) => (a.ordinalPosition ?? 0) - (b.ordinalPosition ?? 0);
|
|
20
|
+
|
|
6
21
|
export const getDatabaseStructure = async (db: Database) => {
|
|
7
|
-
const res = await executeQueryJSONSingle(
|
|
22
|
+
const res = await executeQueryJSONSingle<RawStructure>(
|
|
8
23
|
`
|
|
9
24
|
SELECT JSON_OBJECT(
|
|
10
25
|
'tables', JSON_ARRAYAGG(
|
|
@@ -31,7 +46,7 @@ export const getDatabaseStructure = async (db: Database) => {
|
|
|
31
46
|
)
|
|
32
47
|
)
|
|
33
48
|
FROM information_schema.routines r
|
|
34
|
-
|
|
49
|
+
|
|
35
50
|
-- Parameters
|
|
36
51
|
LEFT JOIN LATERAL (
|
|
37
52
|
SELECT JSON_ARRAYAGG(
|
|
@@ -40,16 +55,16 @@ export const getDatabaseStructure = async (db: Database) => {
|
|
|
40
55
|
'dataType', p.data_type,
|
|
41
56
|
'maxLength', COALESCE(p.character_maximum_length, 0),
|
|
42
57
|
'precision', COALESCE(p.numeric_precision, 0),
|
|
43
|
-
'scale', COALESCE(p.numeric_scale, 0)
|
|
58
|
+
'scale', COALESCE(p.numeric_scale, 0),
|
|
59
|
+
'ordinalPosition', p.ordinal_position
|
|
44
60
|
)
|
|
45
61
|
) AS param_array
|
|
46
62
|
FROM information_schema.parameters p
|
|
47
63
|
WHERE p.specific_name = r.specific_name
|
|
48
64
|
AND p.specific_schema = r.routine_schema
|
|
49
65
|
AND p.parameter_name IS NOT NULL
|
|
50
|
-
ORDER BY p.ordinal_position
|
|
51
66
|
) params ON true
|
|
52
|
-
|
|
67
|
+
|
|
53
68
|
WHERE r.routine_type IN ('FUNCTION', 'PROCEDURE')
|
|
54
69
|
AND r.routine_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
|
|
55
70
|
),
|
|
@@ -58,21 +73,20 @@ export const getDatabaseStructure = async (db: Database) => {
|
|
|
58
73
|
) as json_result
|
|
59
74
|
FROM (
|
|
60
75
|
SELECT table_schema, table_name, table_type
|
|
61
|
-
FROM information_schema.tables
|
|
76
|
+
FROM information_schema.tables
|
|
62
77
|
WHERE table_type IN ('BASE TABLE', 'VIEW')
|
|
63
78
|
AND table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
|
|
64
79
|
) t
|
|
65
80
|
LEFT JOIN LATERAL (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
JSON_ARRAY()
|
|
81
|
+
SELECT JSON_ARRAYAGG(
|
|
82
|
+
JSON_OBJECT(
|
|
83
|
+
'name', isc.column_name,
|
|
84
|
+
'dataType', isc.data_type,
|
|
85
|
+
'isNullable', CASE WHEN isc.is_nullable = 'YES' THEN true ELSE false END,
|
|
86
|
+
'collation', isc.collation_name,
|
|
87
|
+
'description', NULL,
|
|
88
|
+
'ordinalPosition', isc.ordinal_position
|
|
89
|
+
)
|
|
76
90
|
) AS columns
|
|
77
91
|
FROM information_schema.columns isc
|
|
78
92
|
WHERE isc.table_schema = t.table_schema
|
|
@@ -80,26 +94,28 @@ export const getDatabaseStructure = async (db: Database) => {
|
|
|
80
94
|
) col ON TRUE
|
|
81
95
|
LEFT JOIN LATERAL (
|
|
82
96
|
-- aggregate FKs: first aggregate columns per constraint, then aggregate constraints
|
|
83
|
-
SELECT
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
JSON_ARRAY()
|
|
97
|
+
SELECT JSON_ARRAYAGG(
|
|
98
|
+
JSON_OBJECT(
|
|
99
|
+
'schema', fk_sub.referenced_table_schema,
|
|
100
|
+
'name', fk_sub.referenced_table_name,
|
|
101
|
+
'columns', fk_sub.columns_arr,
|
|
102
|
+
'fkName', fk_sub.fk_name
|
|
103
|
+
)
|
|
91
104
|
) AS fk_agg
|
|
92
105
|
FROM (
|
|
93
|
-
SELECT
|
|
106
|
+
SELECT
|
|
94
107
|
kcu.constraint_name AS fk_name,
|
|
95
108
|
kcu.table_schema,
|
|
96
109
|
kcu.table_name,
|
|
97
110
|
kcu.referenced_table_schema,
|
|
98
111
|
kcu.referenced_table_name,
|
|
99
|
-
|
|
100
|
-
JSON_OBJECT(
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
JSON_ARRAYAGG(
|
|
113
|
+
JSON_OBJECT(
|
|
114
|
+
'source', kcu.column_name,
|
|
115
|
+
'target', kcu.referenced_column_name,
|
|
116
|
+
'ordinalPosition', kcu.ordinal_position
|
|
117
|
+
)
|
|
118
|
+
) AS columns_arr
|
|
103
119
|
FROM information_schema.key_column_usage kcu
|
|
104
120
|
WHERE kcu.referenced_table_schema IS NOT NULL
|
|
105
121
|
AND kcu.table_schema = t.table_schema
|
|
@@ -112,5 +128,21 @@ export const getDatabaseStructure = async (db: Database) => {
|
|
|
112
128
|
db,
|
|
113
129
|
);
|
|
114
130
|
|
|
131
|
+
for (const table of res.tables ?? []) {
|
|
132
|
+
table.columns?.sort(byOrdinal);
|
|
133
|
+
table.foreignKeys?.sort((a, b) => (a.fkName ?? "").localeCompare(b.fkName ?? ""));
|
|
134
|
+
|
|
135
|
+
// TableRelationshipZod is strict, so the ordering keys must go before parsing.
|
|
136
|
+
for (const foreignKey of table.foreignKeys ?? []) {
|
|
137
|
+
foreignKey.columns.sort(byOrdinal);
|
|
138
|
+
for (const column of foreignKey.columns) delete column.ordinalPosition;
|
|
139
|
+
delete foreignKey.fkName;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const storedProcedure of res.storedProcedures ?? []) {
|
|
144
|
+
storedProcedure.parameters?.sort(byOrdinal);
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
return DatabaseStructureZod.parse(res);
|
|
116
148
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { VariableDefinition } from "../../../analyzeQuery/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rewrites the builder's PostgreSQL-style `$n` placeholders to MySQL's `?`.
|
|
5
|
+
*
|
|
6
|
+
* The query builders are shared across engines and number placeholders by index
|
|
7
|
+
* into `variablesDefinition`. Bun's MySQL adapter binds `?` by position of
|
|
8
|
+
* occurrence in the statement text, and the two orders do not match: the WHERE
|
|
9
|
+
* string is interpolated into more than one sub-statement on the aggregate path,
|
|
10
|
+
* nested subqueries are built before the outer WHERE is appended, and a single
|
|
11
|
+
* directive can emit `$2` before `$1` and `$1` twice. Rewriting here — the one
|
|
12
|
+
* point where the finished statement and its values meet — makes text order the
|
|
13
|
+
* binding order by construction.
|
|
14
|
+
*
|
|
15
|
+
* The scan skips quoted runs because generated SQL does carry `$n` inside string
|
|
16
|
+
* literals: a config-supplied relationship condition value is interpolated as a
|
|
17
|
+
* literal, and introspected identifiers may contain `$`.
|
|
18
|
+
*/
|
|
19
|
+
export const toMySQLPlaceholders = (
|
|
20
|
+
query: string,
|
|
21
|
+
variablesDefinition: VariableDefinition[] = [],
|
|
22
|
+
values: Record<string, unknown> = {},
|
|
23
|
+
): { query: string; params: unknown[] } => {
|
|
24
|
+
const params: unknown[] = [];
|
|
25
|
+
let rewritten = "";
|
|
26
|
+
let index = 0;
|
|
27
|
+
|
|
28
|
+
while (index < query.length) {
|
|
29
|
+
const char = query[index]!;
|
|
30
|
+
|
|
31
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
32
|
+
const start = index;
|
|
33
|
+
index++;
|
|
34
|
+
|
|
35
|
+
while (index < query.length) {
|
|
36
|
+
// Backslash escapes apply inside string literals but never inside a
|
|
37
|
+
// backtick-quoted identifier, where the only escape is a doubled tick.
|
|
38
|
+
if (char !== "`" && query[index] === "\\") {
|
|
39
|
+
index += 2;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (query[index] === char) {
|
|
43
|
+
if (query[index + 1] === char) {
|
|
44
|
+
index += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
index++;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
index++;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
rewritten += query.slice(start, index);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (char === "$") {
|
|
58
|
+
const match = /^\$(\d+)/.exec(query.slice(index));
|
|
59
|
+
|
|
60
|
+
if (match) {
|
|
61
|
+
const position = Number(match[1]);
|
|
62
|
+
const definition = variablesDefinition[position - 1];
|
|
63
|
+
|
|
64
|
+
if (!definition) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`MySQL query references $${position} but only ${variablesDefinition.length} variable${
|
|
67
|
+
variablesDefinition.length === 1 ? " is" : "s are"
|
|
68
|
+
} defined`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
params.push(values[definition.name]);
|
|
73
|
+
rewritten += "?";
|
|
74
|
+
index += match[0].length;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
rewritten += char;
|
|
80
|
+
index++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { query: rewritten, params };
|
|
84
|
+
};
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
wrapIdentifierMySQL,
|
|
20
20
|
} from "../../../common";
|
|
21
21
|
|
|
22
|
+
import { applyDirectives } from "../../../directives";
|
|
23
|
+
|
|
22
24
|
// Generate CTE for aggregations
|
|
23
25
|
const buildAggregationCTE = (
|
|
24
26
|
groupByInfo: GroupByInfo,
|
|
@@ -61,6 +63,7 @@ const buildAggregationCTE = (
|
|
|
61
63
|
// Build the main query for grouped results
|
|
62
64
|
const buildGroupedQuery = (
|
|
63
65
|
entities: MergedEntities,
|
|
66
|
+
variablesDefinition: VariableDefinition[],
|
|
64
67
|
field: SelectionAnalysis,
|
|
65
68
|
groupByInfo: GroupByInfo,
|
|
66
69
|
dottedQuotedName: string,
|
|
@@ -75,7 +78,15 @@ const buildGroupedQuery = (
|
|
|
75
78
|
if (hasKey) {
|
|
76
79
|
// Add key object with group by fields
|
|
77
80
|
const keyFields = keys
|
|
78
|
-
.map((key) =>
|
|
81
|
+
.map((key) => {
|
|
82
|
+
const selector = applyDirectives(
|
|
83
|
+
`${cteAlias}.${wrapIdentifierMySQL(key.name)}`,
|
|
84
|
+
key.directives,
|
|
85
|
+
"mysql",
|
|
86
|
+
variablesDefinition,
|
|
87
|
+
);
|
|
88
|
+
return `'${key.alias || key.name}', ${selector}`;
|
|
89
|
+
})
|
|
79
90
|
.join(", ");
|
|
80
91
|
|
|
81
92
|
selectClauses.push(`'${keyResolved}', JSON_OBJECT(${keyFields})`);
|
|
@@ -99,7 +110,15 @@ const buildGroupedQuery = (
|
|
|
99
110
|
if (itemsSelection?.selections) {
|
|
100
111
|
const itemFields = itemsSelection.selections
|
|
101
112
|
.filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
|
|
102
|
-
.map((sel) =>
|
|
113
|
+
.map((sel) => {
|
|
114
|
+
const selector = applyDirectives(
|
|
115
|
+
`${tableAlias}.${wrapIdentifierMySQL(sel.name)}`,
|
|
116
|
+
sel.directives,
|
|
117
|
+
"mysql",
|
|
118
|
+
variablesDefinition,
|
|
119
|
+
);
|
|
120
|
+
return `'${sel.alias || sel.name}', ${selector}`;
|
|
121
|
+
})
|
|
103
122
|
.join(", ");
|
|
104
123
|
|
|
105
124
|
if (itemFields) {
|
|
@@ -244,6 +263,7 @@ export const buildSQLForField = (
|
|
|
244
263
|
// Handle GROUP BY aggregation query
|
|
245
264
|
const mainQuery = buildGroupedQuery(
|
|
246
265
|
entities,
|
|
266
|
+
variablesDefinition,
|
|
247
267
|
field,
|
|
248
268
|
groupByInfo,
|
|
249
269
|
dottedQuotedName,
|
|
@@ -277,16 +297,41 @@ export const buildSQLForField = (
|
|
|
277
297
|
([name, selector]) => `'${name}', ${selector}`,
|
|
278
298
|
);
|
|
279
299
|
|
|
280
|
-
const fromClause = `FROM ${dottedQuotedName} ${tableAlias}
|
|
300
|
+
const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
|
|
281
301
|
|
|
282
302
|
const orderByClause = buildOrderByClauseMySQL(entities, field, tableAlias);
|
|
283
303
|
const paginationClause = buildPaginationClauseMySQL(field, variablesDefinition);
|
|
284
304
|
|
|
285
305
|
const isArraySelection = !!field.isArray && !withoutArrayWrapper;
|
|
286
306
|
|
|
307
|
+
// JSON_ARRAYAGG accepts no ORDER BY of its own and collapses the rows before
|
|
308
|
+
// LIMIT can trim them, so both ordering and pagination have to happen in a
|
|
309
|
+
// derived table that the aggregate then reads.
|
|
310
|
+
if (isArraySelection && (orderByClause || paginationClause)) {
|
|
311
|
+
const pageAlias = `${tableAlias}_page`;
|
|
312
|
+
// A nested selection is correlated to its parent through the where clause,
|
|
313
|
+
// and a plain derived table may not carry an outer reference. LATERAL lifts
|
|
314
|
+
// that restriction — MySQL 8.0.14 and newer. Unordered, unpaginated nested
|
|
315
|
+
// selections never take this branch, so older servers keep working for them.
|
|
316
|
+
const lateral = parentTableName ? "LATERAL " : "";
|
|
317
|
+
|
|
318
|
+
// NO_MERGE is load-bearing, not a hint about performance: without it MySQL
|
|
319
|
+
// merges the derived table into the outer query and discards its ORDER BY,
|
|
320
|
+
// so the array comes back in storage order. A LIMIT blocks the merge on its
|
|
321
|
+
// own, which is why only the unpaginated ordering was affected.
|
|
322
|
+
return `
|
|
323
|
+
COALESCE((
|
|
324
|
+
SELECT /*+ NO_MERGE(${pageAlias}) */ JSON_ARRAYAGG(${pageAlias}.obj)
|
|
325
|
+
FROM ${lateral}(
|
|
326
|
+
SELECT JSON_OBJECT(${selectList}) AS obj ${fromClause} ${whereClause} ${orderByClause} ${paginationClause}
|
|
327
|
+
) ${pageAlias}
|
|
328
|
+
), JSON_ARRAY())
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
|
|
287
332
|
return `
|
|
288
333
|
COALESCE((
|
|
289
|
-
SELECT ${isArraySelection ? `JSON_ARRAYAGG(JSON_OBJECT(${selectList}))` : `JSON_OBJECT(${selectList})`} ${fromClause} ${
|
|
334
|
+
SELECT ${isArraySelection ? `JSON_ARRAYAGG(JSON_OBJECT(${selectList}))` : `JSON_OBJECT(${selectList})`} ${fromClause} ${whereClause} ${orderByClause}${withoutArrayWrapper ? " LIMIT 1" : ""}
|
|
290
335
|
), ${isArraySelection ? "JSON_ARRAY()" : "null"})
|
|
291
336
|
`;
|
|
292
337
|
};
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
processFieldSelectionsPG,
|
|
19
19
|
} from "../../../common";
|
|
20
20
|
|
|
21
|
+
import { applyDirectives } from "../../../directives";
|
|
22
|
+
|
|
21
23
|
// Generate CTE for aggregations
|
|
22
24
|
const buildAggregationCTE = (
|
|
23
25
|
groupByInfo: GroupByInfo,
|
|
@@ -58,6 +60,7 @@ const buildAggregationCTE = (
|
|
|
58
60
|
// Build the main query for grouped results
|
|
59
61
|
const buildGroupedQuery = (
|
|
60
62
|
entities: MergedEntities,
|
|
63
|
+
variablesDefinition: VariableDefinition[],
|
|
61
64
|
field: SelectionAnalysis,
|
|
62
65
|
groupByInfo: GroupByInfo,
|
|
63
66
|
dottedQuotedName: string,
|
|
@@ -72,7 +75,15 @@ const buildGroupedQuery = (
|
|
|
72
75
|
if (hasKey) {
|
|
73
76
|
// Add key object with group by fields
|
|
74
77
|
const keyFields = keys
|
|
75
|
-
.map((key) =>
|
|
78
|
+
.map((key) => {
|
|
79
|
+
const selector = applyDirectives(
|
|
80
|
+
`${cteAlias}."${key.name}"`,
|
|
81
|
+
key.directives,
|
|
82
|
+
"pg",
|
|
83
|
+
variablesDefinition,
|
|
84
|
+
);
|
|
85
|
+
return `'${key.alias || key.name}', ${selector}`;
|
|
86
|
+
})
|
|
76
87
|
.join(", ");
|
|
77
88
|
|
|
78
89
|
selectClauses.push(`'${keyResolved}', json_build_object(${keyFields})`);
|
|
@@ -96,7 +107,15 @@ const buildGroupedQuery = (
|
|
|
96
107
|
if (itemsSelection?.selections) {
|
|
97
108
|
const itemFields = itemsSelection.selections
|
|
98
109
|
.filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
|
|
99
|
-
.map((sel) =>
|
|
110
|
+
.map((sel) => {
|
|
111
|
+
const selector = applyDirectives(
|
|
112
|
+
`${tableAlias}."${sel.name}"`,
|
|
113
|
+
sel.directives,
|
|
114
|
+
"pg",
|
|
115
|
+
variablesDefinition,
|
|
116
|
+
);
|
|
117
|
+
return `'${sel.alias || sel.name}', ${selector}`;
|
|
118
|
+
})
|
|
100
119
|
.join(", ");
|
|
101
120
|
|
|
102
121
|
if (itemFields) {
|
|
@@ -240,6 +259,7 @@ export const buildSQLForField = (
|
|
|
240
259
|
// Handle GROUP BY aggregation query
|
|
241
260
|
const mainQuery = buildGroupedQuery(
|
|
242
261
|
entities,
|
|
262
|
+
variablesDefinition,
|
|
243
263
|
field,
|
|
244
264
|
groupByInfo,
|
|
245
265
|
dottedQuotedName,
|
|
@@ -280,6 +300,24 @@ export const buildSQLForField = (
|
|
|
280
300
|
|
|
281
301
|
const isArraySelection = !!field.isArray && !withoutArrayWrapper;
|
|
282
302
|
|
|
303
|
+
// json_agg collapses the rows into one, so LIMIT/OFFSET applied next to it
|
|
304
|
+
// trims aggregate rows rather than table rows: the limit does nothing and any
|
|
305
|
+
// non-zero offset skips the only row there is. Paginate inside a derived table
|
|
306
|
+
// and aggregate that. ROW_NUMBER over the same ordering carries the order out
|
|
307
|
+
// to the aggregate, which PostgreSQL does not otherwise guarantee.
|
|
308
|
+
if (isArraySelection && paginationClause) {
|
|
309
|
+
const pageAlias = `${tableAlias}_page`;
|
|
310
|
+
|
|
311
|
+
return `
|
|
312
|
+
COALESCE((
|
|
313
|
+
SELECT json_agg(${pageAlias}.obj ORDER BY ${pageAlias}.__ord)
|
|
314
|
+
FROM (
|
|
315
|
+
SELECT json_build_object(${selectList}) AS obj, ROW_NUMBER() OVER (${orderByClause}) AS __ord ${fromClause} ${whereClause} ${orderByClause} ${paginationClause}
|
|
316
|
+
) ${pageAlias}
|
|
317
|
+
), '[]'::json)
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
|
|
283
321
|
return `
|
|
284
322
|
COALESCE((
|
|
285
323
|
SELECT ${isArraySelection ? `json_agg(json_build_object(${selectList}) ${orderByClause})` : `json_build_object(${selectList})`} ${fromClause} ${whereClause} ${withoutArrayWrapper ? " LIMIT 1" : ""} ${paginationClause ? ` ${paginationClause}` : ""}
|
|
@@ -105,6 +105,31 @@ export const getDatabasesStructure = async (
|
|
|
105
105
|
target: target.name,
|
|
106
106
|
};
|
|
107
107
|
}),
|
|
108
|
+
conditions: (fk.conditions ?? []).map((cond) => {
|
|
109
|
+
if (cond.source !== undefined) {
|
|
110
|
+
const sourceColumn = t.columns.find(
|
|
111
|
+
(col) => col.name.toLowerCase() === cond.source!.toLowerCase(),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (!sourceColumn) {
|
|
115
|
+
throw new Error(`Condition column ${cond.source} not found in table ${t.name}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { ...cond, source: sourceColumn.name };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const targetColumn = toTable.columns.find(
|
|
122
|
+
(col) => col.name.toLowerCase() === cond.target!.toLowerCase(),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (!targetColumn) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Condition column ${cond.target} not found in table ${toTable.name}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { ...cond, target: targetColumn.name };
|
|
132
|
+
}),
|
|
108
133
|
};
|
|
109
134
|
});
|
|
110
135
|
|
|
@@ -27,7 +27,6 @@ export const categorizeSqlType = (dataType: string): SqlTypeCategory => {
|
|
|
27
27
|
lowerType === "bigint" ||
|
|
28
28
|
lowerType === "tinyint" ||
|
|
29
29
|
lowerType === "mediumint" ||
|
|
30
|
-
lowerType === "numeric" ||
|
|
31
30
|
lowerType === "number"
|
|
32
31
|
) {
|
|
33
32
|
return SqlTypeCategory.INTEGER;
|
|
@@ -40,6 +39,9 @@ export const categorizeSqlType = (dataType: string): SqlTypeCategory => {
|
|
|
40
39
|
lowerType === "double" ||
|
|
41
40
|
lowerType === "double precision" ||
|
|
42
41
|
lowerType === "decimal" ||
|
|
42
|
+
// PostgreSQL's information_schema reports DECIMAL(p, s) as "numeric", so
|
|
43
|
+
// classifying it as an integer typed every PG decimal column as GraphQL Int.
|
|
44
|
+
lowerType === "numeric" ||
|
|
43
45
|
lowerType === "money" ||
|
|
44
46
|
lowerType === "smallmoney"
|
|
45
47
|
) {
|
|
@@ -14,7 +14,7 @@ export const createSubscriptionStrategyRegistry = (): SubscriptionStrategyRegist
|
|
|
14
14
|
const queueStrategy = createQueueSubscriptionStrategy();
|
|
15
15
|
|
|
16
16
|
registry.set(EntitySource.TABLE, databaseStrategy);
|
|
17
|
-
registry.set(EntitySource.
|
|
17
|
+
registry.set(EntitySource.QUEUE_SUBSCRIBER, queueStrategy);
|
|
18
18
|
|
|
19
19
|
return registry;
|
|
20
20
|
};
|
|
@@ -23,7 +23,7 @@ const buildCompositeKey = (context: SubscriptionContext): string => {
|
|
|
23
23
|
* - This strategy manages connection registration with the event emitter
|
|
24
24
|
*/
|
|
25
25
|
export const createQueueSubscriptionStrategy = (): SubscriptionStrategy => ({
|
|
26
|
-
source: EntitySource.
|
|
26
|
+
source: EntitySource.QUEUE_SUBSCRIBER,
|
|
27
27
|
|
|
28
28
|
getSubscriptionKey(context) {
|
|
29
29
|
return buildCompositeKey(context);
|
|
@@ -4,7 +4,13 @@ import type { FilterCondition, OrderByClause } from "../config";
|
|
|
4
4
|
import type { ConfigurationZod } from "./zod/configuration";
|
|
5
5
|
|
|
6
6
|
// Re-export base types from the config module
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
DirectionUnion,
|
|
9
|
+
FilterCondition,
|
|
10
|
+
OrderByClause,
|
|
11
|
+
RelationshipCondition,
|
|
12
|
+
VirtualColumnType,
|
|
13
|
+
} from "../config";
|
|
8
14
|
|
|
9
15
|
export type Configuration = z.infer<typeof ConfigurationZod>;
|
|
10
16
|
|