@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.
Files changed (123) hide show
  1. package/dist/src/analyzeQuery/resolveVariables.d.ts.map +1 -1
  2. package/dist/src/config/helpers/operationHelper.d.ts +60 -6
  3. package/dist/src/config/helpers/operationHelper.d.ts.map +1 -1
  4. package/dist/src/config/helpers/operationHelper.test.d.ts +2 -0
  5. package/dist/src/config/helpers/operationHelper.test.d.ts.map +1 -0
  6. package/dist/src/config/index.d.ts +65 -60
  7. package/dist/src/config/index.d.ts.map +1 -1
  8. package/dist/src/config/types/ai.d.ts +9 -3
  9. package/dist/src/config/types/ai.d.ts.map +1 -1
  10. package/dist/src/config/types/auth.d.ts +55 -15
  11. package/dist/src/config/types/auth.d.ts.map +1 -1
  12. package/dist/src/config/types/cron.d.ts +96 -2
  13. package/dist/src/config/types/cron.d.ts.map +1 -1
  14. package/dist/src/config/types/db.d.ts +186 -24
  15. package/dist/src/config/types/db.d.ts.map +1 -1
  16. package/dist/src/config/types/index.d.ts +3 -3
  17. package/dist/src/config/types/index.d.ts.map +1 -1
  18. package/dist/src/config/types/operation.d.ts +49 -17
  19. package/dist/src/config/types/operation.d.ts.map +1 -1
  20. package/dist/src/config/types/queue.d.ts +59 -12
  21. package/dist/src/config/types/queue.d.ts.map +1 -1
  22. package/dist/src/config/types/remote-rest.d.ts +10 -1
  23. package/dist/src/config/types/remote-rest.d.ts.map +1 -1
  24. package/dist/src/config/types/remote-schema.d.ts +13 -3
  25. package/dist/src/config/types/remote-schema.d.ts.map +1 -1
  26. package/dist/src/config/types/virtual-columns.d.ts +1 -1
  27. package/dist/src/config/types/virtual-columns.d.ts.map +1 -1
  28. package/dist/src/configuration/getSchemas/index.d.ts +52 -4
  29. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  30. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +26 -2
  31. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  32. package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts +2 -0
  33. package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts.map +1 -0
  34. package/dist/src/configuration/getSchemas/type-definition-generator/index.d.ts.map +1 -1
  35. package/dist/src/configuration/index.d.ts +40 -4
  36. package/dist/src/configuration/index.d.ts.map +1 -1
  37. package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
  38. package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
  39. package/dist/src/databases/common.d.ts +3 -1
  40. package/dist/src/databases/common.d.ts.map +1 -1
  41. package/dist/src/databases/core/query-builder.d.ts +7 -1
  42. package/dist/src/databases/core/query-builder.d.ts.map +1 -1
  43. package/dist/src/databases/directives.d.ts.map +1 -1
  44. package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
  45. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  46. package/dist/src/databases/engines/mssql/getStructure.d.ts +7 -0
  47. package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
  48. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  49. package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
  50. package/dist/src/databases/engines/mysql/getStructure.d.ts +7 -0
  51. package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
  52. package/dist/src/databases/engines/mysql/placeholders.d.ts +22 -0
  53. package/dist/src/databases/engines/mysql/placeholders.d.ts.map +1 -0
  54. package/dist/src/databases/engines/mysql/placeholders.test.d.ts +2 -0
  55. package/dist/src/databases/engines/mysql/placeholders.test.d.ts.map +1 -0
  56. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  57. package/dist/src/databases/engines/postgresql/getStructure.d.ts +7 -0
  58. package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
  59. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  60. package/dist/src/databases/high-level-operations.d.ts +14 -1
  61. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  62. package/dist/src/databases/metadata/structure.d.ts +7 -0
  63. package/dist/src/databases/metadata/structure.d.ts.map +1 -1
  64. package/dist/src/databases/sqlTypeUtils.d.ts.map +1 -1
  65. package/dist/src/databases/transformers/data-transformers.d.ts +39 -2
  66. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  67. package/dist/src/singletons/env.d.ts +21 -3
  68. package/dist/src/singletons/env.d.ts.map +1 -1
  69. package/dist/src/types/configuration.d.ts +1 -1
  70. package/dist/src/types/configuration.d.ts.map +1 -1
  71. package/dist/src/types/env.d.ts +127 -51
  72. package/dist/src/types/env.d.ts.map +1 -1
  73. package/dist/src/types/resolver.d.ts +12 -2
  74. package/dist/src/types/resolver.d.ts.map +1 -1
  75. package/dist/src/types/zod/auth.d.ts +6 -6
  76. package/dist/src/types/zod/configuration.d.ts +85 -45
  77. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  78. package/dist/src/types/zod/cron.d.ts +1 -1
  79. package/dist/src/types/zod/cron.d.ts.map +1 -1
  80. package/dist/src/types/zod/db.d.ts +167 -8
  81. package/dist/src/types/zod/db.d.ts.map +1 -1
  82. package/dist/src/types/zod/queue.d.ts +26 -26
  83. package/dist/src/types/zod/strict-keys.test.d.ts +2 -0
  84. package/dist/src/types/zod/strict-keys.test.d.ts.map +1 -0
  85. package/dist/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +1 -1
  87. package/src/analyzeQuery/resolveVariables.ts +37 -7
  88. package/src/config/helpers/operationHelper.ts +146 -5
  89. package/src/config/index.ts +139 -62
  90. package/src/config/types/ai.ts +2 -2
  91. package/src/config/types/auth.ts +37 -6
  92. package/src/config/types/cron.ts +1 -1
  93. package/src/config/types/db.ts +56 -9
  94. package/src/config/types/index.ts +5 -0
  95. package/src/config/types/operation.ts +49 -9
  96. package/src/config/types/queue.ts +8 -8
  97. package/src/config/types/remote-rest.ts +1 -1
  98. package/src/config/types/remote-schema.ts +2 -2
  99. package/src/config/types/virtual-columns.ts +1 -1
  100. package/src/configuration/getSchemas/mergeEntities/index.ts +6 -0
  101. package/src/configuration/getSchemas/type-definition-generator/index.ts +13 -4
  102. package/src/configuration/rest/generateOpenAPI.ts +5 -9
  103. package/src/configuration/rest/handleRESTRequestFactory.ts +40 -14
  104. package/src/databases/common.ts +279 -83
  105. package/src/databases/directives.ts +26 -8
  106. package/src/databases/engines/mssql/connection.ts +1 -1
  107. package/src/databases/engines/mssql/getStructure.ts +1 -0
  108. package/src/databases/engines/mssql/query/index.ts +29 -11
  109. package/src/databases/engines/mysql/auth.ts +2 -2
  110. package/src/databases/engines/mysql/connection.ts +5 -4
  111. package/src/databases/engines/mysql/getStructure.ts +61 -29
  112. package/src/databases/engines/mysql/placeholders.ts +84 -0
  113. package/src/databases/engines/mysql/query/index.ts +49 -4
  114. package/src/databases/engines/postgresql/getStructure.ts +1 -0
  115. package/src/databases/engines/postgresql/query/index.ts +40 -2
  116. package/src/databases/high-level-operations.ts +25 -0
  117. package/src/databases/sqlTypeUtils.ts +3 -1
  118. package/src/subscriptions/strategies/index.ts +1 -1
  119. package/src/subscriptions/strategies/queue.ts +1 -1
  120. package/src/types/configuration.ts +7 -1
  121. package/src/types/resolver.ts +17 -1
  122. package/src/types/zod/configuration.ts +1 -1
  123. 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
- selectClauses.push(
48
- `${func}(${tableAlias}.${wrapIdentifierMSSQL(agg.fieldName)}) AS ${agg.alias}`,
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
- (field) =>
83
- `${cteAlias}.${wrapIdentifierMSSQL(field.name)} AS ${wrapIdentifierMSSQL(field.alias || 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
- (sel) =>
112
- `${tableAlias}.${wrapIdentifierMSSQL(sel.name)} AS ${wrapIdentifierMSSQL(sel.alias || 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 = $1 AND is_active = TRUE`,
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 ($1, $2, $3, TRUE, $4)`,
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
- const result = await pool.unsafe<T>(
46
- query,
47
- variablesDefinition.map((v) => values[v.name]),
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
- -- build an ordered JSON array of columns using GROUP_CONCAT
67
- SELECT IFNULL(
68
- CAST(CONCAT('[', GROUP_CONCAT(
69
- JSON_OBJECT(
70
- 'name', isc.column_name,
71
- 'dataType', isc.data_type,
72
- 'isNullable', CASE WHEN isc.is_nullable = 'YES' THEN true ELSE false END,
73
- 'description', NULL
74
- ) ORDER BY isc.ordinal_position SEPARATOR ','), ']') AS JSON),
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 IFNULL(
84
- CAST(CONCAT('[', GROUP_CONCAT(
85
- JSON_OBJECT(
86
- 'schema', fk_sub.referenced_table_schema,
87
- 'name', fk_sub.referenced_table_name,
88
- 'columns', fk_sub.columns_arr
89
- ) ORDER BY fk_sub.fk_name SEPARATOR ','), ']') AS JSON),
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
- CAST(CONCAT('[', GROUP_CONCAT(
100
- JSON_OBJECT('source', kcu.column_name, 'target', kcu.referenced_column_name)
101
- ORDER BY kcu.ordinal_position SEPARATOR ','
102
- ), ']') AS JSON) AS columns_arr
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) => `'${key.alias || key.name}', ${cteAlias}.${wrapIdentifierMySQL(key.name)}`)
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) => `'${sel.alias || sel.name}', ${tableAlias}.${wrapIdentifierMySQL(sel.name)}`)
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}${withoutArrayWrapper ? " LIMIT 1" : ""}`;
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} ${orderByClause} ${whereClause} ${paginationClause ? ` ${paginationClause}` : ""}
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
  };
@@ -35,6 +35,7 @@ export const getDatabaseStructure = async (db: Database) => {
35
35
  'name', isc.column_name,
36
36
  'dataType', isc.data_type,
37
37
  'isNullable', isc.is_nullable = 'YES',
38
+ 'collation', isc.collation_name,
38
39
  'description', col_desc.description
39
40
  )
40
41
  ) AS columns
@@ -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) => `'${key.alias || key.name}', ${cteAlias}."${key.name}"`)
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) => `'${sel.alias || sel.name}', ${tableAlias}."${sel.name}"`)
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.QUEUE_PUBLISHER, queueStrategy);
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.QUEUE_PUBLISHER,
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 { DirectionUnion, FilterCondition, OrderByClause, VirtualColumnType } from "../config";
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