@graphoria/server 0.3.0 → 0.5.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/README.md +28 -20
- package/dist/src/ai/mcp/index.d.ts +9 -2
- package/dist/src/ai/mcp/index.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/depthLimit.d.ts +1 -13
- package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
- package/dist/src/authentication/capabilities.d.ts +18 -0
- package/dist/src/authentication/capabilities.d.ts.map +1 -0
- package/dist/src/authentication/capabilities.test.d.ts +2 -0
- package/dist/src/authentication/capabilities.test.d.ts.map +1 -0
- package/dist/src/authentication/jwt.d.ts.map +1 -1
- package/dist/src/authentication/paseto.d.ts.map +1 -1
- package/dist/src/authentication/secrets.d.ts +9 -0
- package/dist/src/authentication/secrets.d.ts.map +1 -0
- package/dist/src/authentication/secrets.test.d.ts +2 -0
- package/dist/src/authentication/secrets.test.d.ts.map +1 -0
- package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
- package/dist/src/config/types/db.d.ts +25 -0
- package/dist/src/config/types/db.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/index.d.ts +40 -2
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +20 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
- package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
- package/dist/src/configuration/index.d.ts +30 -1
- 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/generateOpenAPI.test.d.ts +2 -0
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts.map +1 -0
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/console/api.d.ts.map +1 -1
- package/dist/src/console/session.d.ts +12 -4
- package/dist/src/console/session.d.ts.map +1 -1
- package/dist/src/cron/index.d.ts.map +1 -1
- package/dist/src/databases/common.d.ts +20 -6
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/core/executor.d.ts +4 -6
- package/dist/src/databases/core/executor.d.ts.map +1 -1
- package/dist/src/databases/core/function-mapping.d.ts +2 -3
- package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
- package/dist/src/databases/core/procedure-arguments.d.ts +17 -0
- package/dist/src/databases/core/procedure-arguments.d.ts.map +1 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts +2 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts.map +1 -0
- package/dist/src/databases/core/query-builder.d.ts +5 -0
- package/dist/src/databases/core/query-builder.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 +5 -0
- package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
- package/dist/src/databases/high-level-operations.d.ts +5 -0
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/metadata/structure.d.ts +5 -0
- package/dist/src/databases/metadata/structure.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +30 -0
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logging/audit.d.ts +27 -0
- package/dist/src/logging/audit.d.ts.map +1 -0
- package/dist/src/logging/audit.test.d.ts +2 -0
- package/dist/src/logging/audit.test.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.d.ts +29 -0
- package/dist/src/logging/slowQuery.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.test.d.ts +2 -0
- package/dist/src/logging/slowQuery.test.d.ts.map +1 -0
- package/dist/src/observability/health.d.ts +23 -0
- package/dist/src/observability/health.d.ts.map +1 -0
- package/dist/src/observability/health.test.d.ts +2 -0
- package/dist/src/observability/health.test.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.d.ts +7 -0
- package/dist/src/observability/httpMetrics.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.test.d.ts +2 -0
- package/dist/src/observability/httpMetrics.test.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.d.ts +11 -0
- package/dist/src/observability/httpTracing.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.test.d.ts +2 -0
- package/dist/src/observability/httpTracing.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.d.ts +81 -0
- package/dist/src/observability/metrics.d.ts.map +1 -0
- package/dist/src/observability/metrics.perf.test.d.ts +2 -0
- package/dist/src/observability/metrics.perf.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.test.d.ts +2 -0
- package/dist/src/observability/metrics.test.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.d.ts +19 -0
- package/dist/src/observability/metricsRoute.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.test.d.ts +2 -0
- package/dist/src/observability/metricsRoute.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.d.ts +92 -0
- package/dist/src/observability/tracing.d.ts.map +1 -0
- package/dist/src/observability/tracing.perf.test.d.ts +2 -0
- package/dist/src/observability/tracing.perf.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.test.d.ts +2 -0
- package/dist/src/observability/tracing.test.d.ts.map +1 -0
- package/dist/src/queues/kafka.d.ts +6 -0
- package/dist/src/queues/kafka.d.ts.map +1 -1
- package/dist/src/queues/kafka.test.d.ts +2 -0
- package/dist/src/queues/kafka.test.d.ts.map +1 -0
- package/dist/src/queues/rabbitmq.d.ts.map +1 -1
- package/dist/src/remoteREST/proxy.d.ts.map +1 -1
- package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
- package/dist/src/singletons/cache/redisClient.d.ts +2 -2
- package/dist/src/singletons/cache/redisClient.d.ts.map +1 -1
- package/dist/src/singletons/databases.d.ts +1 -0
- package/dist/src/singletons/databases.d.ts.map +1 -1
- package/dist/src/singletons/env.d.ts +37 -4
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/singletons/queues.d.ts +1 -0
- package/dist/src/singletons/queues.d.ts.map +1 -1
- package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.d.ts +1 -0
- package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.test.d.ts +2 -0
- package/dist/src/subscriptions/utils/polling.test.d.ts.map +1 -0
- package/dist/src/types/env.d.ts +113 -12
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/types/zod/configuration.d.ts +25 -0
- package/dist/src/types/zod/configuration.d.ts.map +1 -1
- package/dist/src/types/zod/db.d.ts +85 -0
- package/dist/src/types/zod/db.d.ts.map +1 -1
- package/dist/src/utils/rateLimit.d.ts.map +1 -1
- package/dist/src/utils/redis.d.ts +17 -0
- package/dist/src/utils/redis.d.ts.map +1 -0
- package/dist/src/utils/redis.test.d.ts +2 -0
- package/dist/src/utils/redis.test.d.ts.map +1 -0
- package/dist/src/utils/responses.d.ts +6 -0
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/sessionVariables.d.ts +2 -0
- package/dist/src/utils/sessionVariables.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -5
- package/playgrounds/console/index.html +9 -9
- package/playgrounds/graphiql/index.html +549 -686
- package/playgrounds/scalar/index.html +1 -1
- package/src/ai/mcp/index.ts +19 -14
- package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +2 -1
- package/src/analyzeQuery/depthLimit.ts +15 -5
- package/src/authentication/capabilities.ts +56 -0
- package/src/authentication/index.ts +3 -3
- package/src/authentication/jwt.ts +30 -25
- package/src/authentication/paseto.ts +36 -26
- package/src/authentication/secrets.ts +30 -0
- package/src/authentication/tokenRepository.ts +2 -3
- package/src/config/types/db.ts +16 -2
- package/src/configuration/getSchemas/mergeEntities/index.ts +0 -26
- package/src/configuration/gql/gqlAuthOperations.ts +26 -0
- package/src/configuration/gql/handleGraphQLRequestFactory.ts +241 -148
- package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +9 -0
- package/src/configuration/rest/generateOpenAPI.ts +61 -11
- package/src/configuration/rest/handleRESTRequestFactory.ts +26 -0
- package/src/console/api.ts +64 -24
- package/src/console/session.ts +30 -26
- package/src/cron/index.ts +59 -36
- package/src/databases/common.ts +109 -10
- package/src/databases/core/executor.ts +97 -20
- package/src/databases/core/function-mapping.ts +1 -2
- package/src/databases/core/procedure-arguments.ts +25 -0
- package/src/databases/engines/mssql/connection.ts +0 -3
- package/src/databases/engines/mssql/query/index.ts +4 -1
- package/src/databases/engines/mysql/connection.ts +10 -7
- package/src/databases/engines/mysql/query/index.ts +5 -2
- package/src/databases/engines/postgresql/connection.ts +10 -7
- package/src/databases/engines/postgresql/query/index.ts +5 -2
- package/src/databases/high-level-operations.ts +44 -2
- package/src/index.ts +200 -76
- package/src/logging/audit.ts +86 -0
- package/src/logging/slowQuery.ts +59 -0
- package/src/observability/health.ts +67 -0
- package/src/observability/httpMetrics.ts +37 -0
- package/src/observability/httpTracing.ts +48 -0
- package/src/observability/metrics.ts +243 -0
- package/src/observability/metricsRoute.ts +33 -0
- package/src/observability/tracing.ts +421 -0
- package/src/queues/kafka.ts +45 -2
- package/src/queues/rabbitmq.ts +37 -1
- package/src/remoteREST/proxy.ts +17 -0
- package/src/remoteSchemas/proxy.ts +17 -0
- package/src/singletons/cache/redisClient.ts +4 -5
- package/src/singletons/databases.ts +5 -0
- package/src/singletons/queues.ts +9 -3
- package/src/subscriptions/strategies/database.ts +2 -0
- package/src/subscriptions/utils/polling.ts +80 -21
- package/src/types/env.ts +53 -8
- package/src/utils/rateLimit.ts +6 -1
- package/src/utils/redis.ts +71 -0
- package/src/utils/responses.ts +12 -0
- package/src/utils/sessionVariables.ts +2 -0
package/src/databases/common.ts
CHANGED
|
@@ -59,6 +59,18 @@ const resolveBooleanOperand = (
|
|
|
59
59
|
|
|
60
60
|
export const generateTableAlias = (level: number): string => `t${level}`;
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A table whose alias is lexically in scope above the join being emitted, outermost
|
|
64
|
+
* first and including the immediate parent. Nested selections compile to correlated
|
|
65
|
+
* subqueries, so every enclosing alias stays referenceable — this is what lets a
|
|
66
|
+
* relationship condition name a column on an ancestor rather than a literal.
|
|
67
|
+
*
|
|
68
|
+
* Built fresh at each recursion rather than read off `aliasMap`: that map is flat,
|
|
69
|
+
* shared and written by two different level counters, so it cannot distinguish an
|
|
70
|
+
* enclosing scope from a sibling that merely ran earlier.
|
|
71
|
+
*/
|
|
72
|
+
export type QueryPathFrame = { table: string; alias: string };
|
|
73
|
+
|
|
62
74
|
const mappingDbTypeCharVar: Record<DatabaseType, string> = {
|
|
63
75
|
pg: "$",
|
|
64
76
|
mysql: "$",
|
|
@@ -76,6 +88,7 @@ export const buildConditions = (
|
|
|
76
88
|
aliasMap: { [alias: string]: string },
|
|
77
89
|
/** Query name of the table being filtered — resolves its virtual columns. */
|
|
78
90
|
tableName?: string,
|
|
91
|
+
ancestors: readonly QueryPathFrame[] = [],
|
|
79
92
|
): string => {
|
|
80
93
|
if (!whereArgs) return "";
|
|
81
94
|
|
|
@@ -135,6 +148,13 @@ export const buildConditions = (
|
|
|
135
148
|
|
|
136
149
|
const parentTableName = aliasMap[tableAlias];
|
|
137
150
|
|
|
151
|
+
// An EXISTS sits inside the WHERE of the block that binds `tableAlias`, so that
|
|
152
|
+
// block encloses it just as a selection subquery would. Extending the scope here
|
|
153
|
+
// is what makes a relation filter emit the same predicate as the selection form.
|
|
154
|
+
const scope = parentTableName
|
|
155
|
+
? [...ancestors, { table: parentTableName, alias: tableAlias }]
|
|
156
|
+
: ancestors;
|
|
157
|
+
|
|
138
158
|
const joinCondition = findJoinCondition(
|
|
139
159
|
entities,
|
|
140
160
|
parentTableName,
|
|
@@ -142,6 +162,7 @@ export const buildConditions = (
|
|
|
142
162
|
tableAlias,
|
|
143
163
|
nestedTableAlias,
|
|
144
164
|
dbType,
|
|
165
|
+
scope,
|
|
145
166
|
);
|
|
146
167
|
|
|
147
168
|
// Record this relation's alias→table so a deeper nested where (3+ levels) can resolve
|
|
@@ -159,6 +180,7 @@ export const buildConditions = (
|
|
|
159
180
|
level + 1,
|
|
160
181
|
aliasMap,
|
|
161
182
|
fieldName,
|
|
183
|
+
scope,
|
|
162
184
|
);
|
|
163
185
|
|
|
164
186
|
const existsClause = `EXISTS (
|
|
@@ -324,6 +346,47 @@ const columnCollation = (
|
|
|
324
346
|
return column && "collation" in column ? column.collation : undefined;
|
|
325
347
|
};
|
|
326
348
|
|
|
349
|
+
// Resolves an `ancestor` condition to a qualified column on an enclosing alias.
|
|
350
|
+
// Matches on entity identity rather than on the frame's key, because a frame may be
|
|
351
|
+
// keyed by a suffixed relationship name (`app_users_ref`) and because resolverName
|
|
352
|
+
// depends on the database's configurable `fieldNaming` — neither is comparable to
|
|
353
|
+
// the schema/name pair the config declares. Nearest enclosing match wins, so a
|
|
354
|
+
// self-referential chain binds to the closest occurrence.
|
|
355
|
+
const ancestorOperand = (
|
|
356
|
+
entities: MergedEntities,
|
|
357
|
+
ancestors: readonly QueryPathFrame[],
|
|
358
|
+
ancestor: { schema: string; name: string; column: string },
|
|
359
|
+
leftTable: string,
|
|
360
|
+
leftColumn: string,
|
|
361
|
+
dbType: DatabaseType,
|
|
362
|
+
): { sql: string; collate: string } => {
|
|
363
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
364
|
+
const frame = ancestors[i]!;
|
|
365
|
+
const entity = entities.queriesMap[frame.table];
|
|
366
|
+
|
|
367
|
+
if (entity?.schema !== ancestor.schema || entity.name !== ancestor.name) continue;
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
sql: `${frame.alias}.${wrapIdentifierFp(dbType)(ancestor.column)}`,
|
|
371
|
+
// Unlike a literal comparison, this equates two character columns that may
|
|
372
|
+
// carry different collations — the case collateSuffix exists for.
|
|
373
|
+
collate: collateSuffix(
|
|
374
|
+
dbType,
|
|
375
|
+
columnCollation(entities, leftTable, leftColumn),
|
|
376
|
+
columnCollation(entities, frame.table, ancestor.column),
|
|
377
|
+
),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const inScope = ancestors.map((a) => a.table).join(" > ");
|
|
382
|
+
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Relationship condition references ancestor table "${ancestor.schema}.${ancestor.name}", which is not in this query's path. ` +
|
|
385
|
+
`In scope here: ${inScope || "(none)"}. ` +
|
|
386
|
+
`Nest this selection under ${ancestor.schema}.${ancestor.name}, or drop the "ancestor" condition.`,
|
|
387
|
+
);
|
|
388
|
+
};
|
|
389
|
+
|
|
327
390
|
const pairsToAnd = (
|
|
328
391
|
pairs: {
|
|
329
392
|
parentAlias: string;
|
|
@@ -377,6 +440,7 @@ const renderCondition = (
|
|
|
377
440
|
column: string,
|
|
378
441
|
operator: string,
|
|
379
442
|
value: string | number | boolean | undefined,
|
|
443
|
+
ancestor?: { sql: string; collate: string },
|
|
380
444
|
): string => {
|
|
381
445
|
const target = `${alias}.${wrapIdentifierFp(dbType)(column)}`;
|
|
382
446
|
|
|
@@ -384,7 +448,13 @@ const renderCondition = (
|
|
|
384
448
|
if (operator === "is_not_null") return `${target} IS NOT NULL`;
|
|
385
449
|
|
|
386
450
|
const sqlOperator = RELATIONSHIP_CONDITION_SQL_OPERATORS[operator];
|
|
387
|
-
if (!sqlOperator
|
|
451
|
+
if (!sqlOperator) {
|
|
452
|
+
throw new Error(`Invalid relationship condition operator "${operator}"`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (ancestor) return `${target} ${sqlOperator} ${ancestor.sql}${ancestor.collate}`;
|
|
456
|
+
|
|
457
|
+
if (value === undefined) {
|
|
388
458
|
throw new Error(`Invalid relationship condition operator "${operator}"`);
|
|
389
459
|
}
|
|
390
460
|
|
|
@@ -396,16 +466,25 @@ const renderCondition = (
|
|
|
396
466
|
// toAlias). The from/to aliases flip between forward and reverse joins.
|
|
397
467
|
const conditionsToAnd = (
|
|
398
468
|
conditions: readonly RelationshipCondition[] | undefined,
|
|
399
|
-
|
|
400
|
-
|
|
469
|
+
from: { alias: string; table: string },
|
|
470
|
+
to: { alias: string; table: string },
|
|
401
471
|
dbType: DatabaseType,
|
|
472
|
+
entities: MergedEntities,
|
|
473
|
+
ancestors: readonly QueryPathFrame[],
|
|
402
474
|
): string =>
|
|
403
475
|
(conditions ?? [])
|
|
404
|
-
.map((c) =>
|
|
405
|
-
c.source !== undefined
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
476
|
+
.map((c) => {
|
|
477
|
+
const side = c.source !== undefined ? from : to;
|
|
478
|
+
const column = (c.source ?? c.target)!;
|
|
479
|
+
|
|
480
|
+
// The from/to flip above picks the left operand only; an ancestor is the right
|
|
481
|
+
// operand and resolves by table identity, so it is direction-independent.
|
|
482
|
+
const ancestor = c.ancestor
|
|
483
|
+
? ancestorOperand(entities, ancestors, c.ancestor, side.table, column, dbType)
|
|
484
|
+
: undefined;
|
|
485
|
+
|
|
486
|
+
return renderCondition(dbType, side.alias, column, c.operator ?? "eq", c.value, ancestor);
|
|
487
|
+
})
|
|
409
488
|
.join(" AND ");
|
|
410
489
|
|
|
411
490
|
const joinParts = (...parts: string[]): string => parts.filter(Boolean).join(" AND ");
|
|
@@ -417,6 +496,7 @@ export const findJoinCondition = (
|
|
|
417
496
|
parentAlias: string,
|
|
418
497
|
childAlias: string,
|
|
419
498
|
dbType: DatabaseType,
|
|
499
|
+
ancestors: readonly QueryPathFrame[] = [],
|
|
420
500
|
): string => {
|
|
421
501
|
const relations = entities.getForeignKeysBetweenTables(parentTableName, childTableName);
|
|
422
502
|
|
|
@@ -436,7 +516,14 @@ export const findJoinCondition = (
|
|
|
436
516
|
})),
|
|
437
517
|
dbType,
|
|
438
518
|
),
|
|
439
|
-
conditionsToAnd(
|
|
519
|
+
conditionsToAnd(
|
|
520
|
+
relation.conditions,
|
|
521
|
+
{ alias: parentAlias, table: parentTableName },
|
|
522
|
+
{ alias: childAlias, table: childTableName },
|
|
523
|
+
dbType,
|
|
524
|
+
entities,
|
|
525
|
+
ancestors,
|
|
526
|
+
),
|
|
440
527
|
),
|
|
441
528
|
) ?? []),
|
|
442
529
|
...(relations.relationshipsReversed?.map((relation) =>
|
|
@@ -452,7 +539,14 @@ export const findJoinCondition = (
|
|
|
452
539
|
})),
|
|
453
540
|
dbType,
|
|
454
541
|
),
|
|
455
|
-
conditionsToAnd(
|
|
542
|
+
conditionsToAnd(
|
|
543
|
+
relation.conditions,
|
|
544
|
+
{ alias: childAlias, table: childTableName },
|
|
545
|
+
{ alias: parentAlias, table: parentTableName },
|
|
546
|
+
dbType,
|
|
547
|
+
entities,
|
|
548
|
+
ancestors,
|
|
549
|
+
),
|
|
456
550
|
),
|
|
457
551
|
) ?? []),
|
|
458
552
|
].join(" AND ");
|
|
@@ -470,6 +564,7 @@ export const buildWhereClauseFp =
|
|
|
470
564
|
parentTableAlias: string | null,
|
|
471
565
|
level: number,
|
|
472
566
|
aliasMap: { [alias: string]: string },
|
|
567
|
+
ancestors: readonly QueryPathFrame[] = [],
|
|
473
568
|
): string => {
|
|
474
569
|
const whereConditions: string[] = [
|
|
475
570
|
buildConditions(
|
|
@@ -482,7 +577,10 @@ export const buildWhereClauseFp =
|
|
|
482
577
|
level + 1,
|
|
483
578
|
aliasMap,
|
|
484
579
|
field.name ?? aliasMap[tableAlias],
|
|
580
|
+
ancestors,
|
|
485
581
|
),
|
|
582
|
+
// `ancestors` already ends with this join's parent, and the child is the field
|
|
583
|
+
// being emitted, so the scope needs no splicing here.
|
|
486
584
|
parentTableName && parentTableAlias
|
|
487
585
|
? findJoinCondition(
|
|
488
586
|
entities,
|
|
@@ -491,6 +589,7 @@ export const buildWhereClauseFp =
|
|
|
491
589
|
parentTableAlias,
|
|
492
590
|
tableAlias,
|
|
493
591
|
dbType,
|
|
592
|
+
ancestors,
|
|
494
593
|
)
|
|
495
594
|
: null,
|
|
496
595
|
].filter(Boolean) as string[];
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import type { VariableDefinition } from "../../analyzeQuery/types";
|
|
2
|
+
import type { QuerySource } from "../../logging/slowQuery";
|
|
2
3
|
import type { Database } from "../../types/configuration";
|
|
3
4
|
import type { ProcedureResolver } from "../../types/db";
|
|
4
5
|
|
|
5
6
|
import { databaseAdapters } from "./function-mapping";
|
|
6
7
|
import { logger } from "../../logging";
|
|
8
|
+
import { reportQueryDuration } from "../../logging/slowQuery";
|
|
9
|
+
import { startSpan } from "../../observability/tracing";
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Core database execution functions
|
|
10
13
|
*/
|
|
11
14
|
|
|
15
|
+
/** Names only. The variable values are caller data and never reach a span. */
|
|
16
|
+
const sourceAttributes = (source?: QuerySource) => ({
|
|
17
|
+
"graphql.operation.name": source?.operation.name ?? undefined,
|
|
18
|
+
"graphql.operation.type": source?.operation.type,
|
|
19
|
+
"graphoria.role": source?.role,
|
|
20
|
+
});
|
|
21
|
+
|
|
12
22
|
export const executeQuery = async <T>(
|
|
13
23
|
query: string,
|
|
14
24
|
db: Database,
|
|
@@ -16,6 +26,7 @@ export const executeQuery = async <T>(
|
|
|
16
26
|
variables: Record<string, unknown>,
|
|
17
27
|
/** Per-operation override. `undefined` leaves the engine on its own default. */
|
|
18
28
|
timeoutMs?: number,
|
|
29
|
+
source?: QuerySource,
|
|
19
30
|
) => {
|
|
20
31
|
const adapter = databaseAdapters[db?.type];
|
|
21
32
|
if (!adapter) {
|
|
@@ -24,16 +35,40 @@ export const executeQuery = async <T>(
|
|
|
24
35
|
|
|
25
36
|
const log = logger("db").child({ dbType: db.type, dbName: db.name });
|
|
26
37
|
const startTime = Bun.nanoseconds();
|
|
38
|
+
const report = (durationMs: number, outcome: "success" | "error") =>
|
|
39
|
+
reportQueryDuration({
|
|
40
|
+
durationMs,
|
|
41
|
+
dbType: db.type,
|
|
42
|
+
dbName: db.name,
|
|
43
|
+
sql: query,
|
|
44
|
+
outcome,
|
|
45
|
+
...source,
|
|
46
|
+
});
|
|
47
|
+
// Every literal is hoisted into a bound parameter, so the statement text
|
|
48
|
+
// carries no caller data and is safe to export.
|
|
49
|
+
const span = startSpan("db.query", {
|
|
50
|
+
kind: "client",
|
|
51
|
+
attributes: {
|
|
52
|
+
"db.system": db.type,
|
|
53
|
+
"db.name": db.name,
|
|
54
|
+
"db.statement": query,
|
|
55
|
+
...sourceAttributes(source),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
27
58
|
|
|
28
59
|
try {
|
|
29
60
|
const result = await adapter.execute<T>(query, db, variablesDefinition, variables, timeoutMs);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
61
|
+
const durationMs = (Bun.nanoseconds() - startTime) / 1e6;
|
|
62
|
+
log.debug({ durationMs, queryLength: query.length }, "query executed");
|
|
63
|
+
report(durationMs, "success");
|
|
64
|
+
span?.end();
|
|
34
65
|
return result;
|
|
35
66
|
} catch (error) {
|
|
36
|
-
|
|
67
|
+
const durationMs = (Bun.nanoseconds() - startTime) / 1e6;
|
|
68
|
+
log.error({ err: error, durationMs }, "query failed");
|
|
69
|
+
report(durationMs, "error");
|
|
70
|
+
span?.recordError(error);
|
|
71
|
+
span?.end();
|
|
37
72
|
throw error;
|
|
38
73
|
}
|
|
39
74
|
};
|
|
@@ -45,6 +80,7 @@ export const executeQueryJSON = async <T>(
|
|
|
45
80
|
variables: Record<string, unknown>,
|
|
46
81
|
/** Per-operation override. `undefined` leaves the engine on its own default. */
|
|
47
82
|
timeoutMs?: number,
|
|
83
|
+
source?: QuerySource,
|
|
48
84
|
) => {
|
|
49
85
|
const adapter = databaseAdapters[db.type];
|
|
50
86
|
if (!adapter) {
|
|
@@ -53,6 +89,24 @@ export const executeQueryJSON = async <T>(
|
|
|
53
89
|
|
|
54
90
|
const log = logger("db").child({ dbType: db.type, dbName: db.name });
|
|
55
91
|
const startTime = Bun.nanoseconds();
|
|
92
|
+
const report = (durationMs: number, outcome: "success" | "error") =>
|
|
93
|
+
reportQueryDuration({
|
|
94
|
+
durationMs,
|
|
95
|
+
dbType: db.type,
|
|
96
|
+
dbName: db.name,
|
|
97
|
+
sql: query,
|
|
98
|
+
outcome,
|
|
99
|
+
...source,
|
|
100
|
+
});
|
|
101
|
+
const span = startSpan("db.query", {
|
|
102
|
+
kind: "client",
|
|
103
|
+
attributes: {
|
|
104
|
+
"db.system": db.type,
|
|
105
|
+
"db.name": db.name,
|
|
106
|
+
"db.statement": query,
|
|
107
|
+
...sourceAttributes(source),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
56
110
|
|
|
57
111
|
try {
|
|
58
112
|
const result = await adapter.executeJson<T>(
|
|
@@ -62,24 +116,25 @@ export const executeQueryJSON = async <T>(
|
|
|
62
116
|
variables,
|
|
63
117
|
timeoutMs,
|
|
64
118
|
);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
119
|
+
const durationMs = (Bun.nanoseconds() - startTime) / 1e6;
|
|
120
|
+
log.debug({ durationMs, queryLength: query.length }, "query executed (json)");
|
|
121
|
+
report(durationMs, "success");
|
|
122
|
+
span?.end();
|
|
69
123
|
return result;
|
|
70
124
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
125
|
+
const durationMs = (Bun.nanoseconds() - startTime) / 1e6;
|
|
126
|
+
log.error({ err: error, durationMs }, "query failed (json)");
|
|
127
|
+
report(durationMs, "error");
|
|
128
|
+
span?.recordError(error);
|
|
129
|
+
span?.end();
|
|
75
130
|
throw error;
|
|
76
131
|
}
|
|
77
132
|
};
|
|
78
133
|
|
|
79
134
|
export const callStoredProcedure = async (
|
|
80
135
|
sp: ProcedureResolver,
|
|
81
|
-
variablesDefinition: VariableDefinition[],
|
|
82
136
|
variables: Record<string, unknown> = {},
|
|
137
|
+
source?: QuerySource,
|
|
83
138
|
) => {
|
|
84
139
|
const adapter = databaseAdapters[sp.db!.type];
|
|
85
140
|
if (!adapter) {
|
|
@@ -92,16 +147,38 @@ export const callStoredProcedure = async (
|
|
|
92
147
|
procedure: sp.dottedName,
|
|
93
148
|
});
|
|
94
149
|
const startTime = Bun.nanoseconds();
|
|
150
|
+
const report = (durationMs: number, outcome: "success" | "error") =>
|
|
151
|
+
reportQueryDuration({
|
|
152
|
+
durationMs,
|
|
153
|
+
dbType: sp.db!.type,
|
|
154
|
+
dbName: sp.db!.name,
|
|
155
|
+
procedure: sp.dottedName,
|
|
156
|
+
outcome,
|
|
157
|
+
...source,
|
|
158
|
+
});
|
|
159
|
+
const span = startSpan("db.procedure", {
|
|
160
|
+
kind: "client",
|
|
161
|
+
attributes: {
|
|
162
|
+
"db.system": sp.db!.type,
|
|
163
|
+
"db.name": sp.db!.name,
|
|
164
|
+
"db.operation": sp.dottedName,
|
|
165
|
+
...sourceAttributes(source),
|
|
166
|
+
},
|
|
167
|
+
});
|
|
95
168
|
|
|
96
169
|
try {
|
|
97
|
-
const result = await adapter.callStoredProcedure(sp,
|
|
98
|
-
|
|
170
|
+
const result = await adapter.callStoredProcedure(sp, variables);
|
|
171
|
+
const durationMs = (Bun.nanoseconds() - startTime) / 1e6;
|
|
172
|
+
log.debug({ durationMs }, "stored procedure executed");
|
|
173
|
+
report(durationMs, "success");
|
|
174
|
+
span?.end();
|
|
99
175
|
return result;
|
|
100
176
|
} catch (error) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
177
|
+
const durationMs = (Bun.nanoseconds() - startTime) / 1e6;
|
|
178
|
+
log.error({ err: error, durationMs }, "stored procedure failed");
|
|
179
|
+
report(durationMs, "error");
|
|
180
|
+
span?.recordError(error);
|
|
181
|
+
span?.end();
|
|
105
182
|
throw error;
|
|
106
183
|
}
|
|
107
184
|
};
|
|
@@ -80,7 +80,7 @@ export type InsertAuthUserInput = {
|
|
|
80
80
|
claims?: Record<string, unknown>;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
type DatabaseFunctions = {
|
|
83
|
+
export type DatabaseFunctions = {
|
|
84
84
|
query: (
|
|
85
85
|
entities: MergedEntities,
|
|
86
86
|
operation: OperationAnalysis,
|
|
@@ -105,7 +105,6 @@ type DatabaseFunctions = {
|
|
|
105
105
|
) => Promise<T>;
|
|
106
106
|
callStoredProcedure: (
|
|
107
107
|
sp: ProcedureResolver,
|
|
108
|
-
variablesDefinition: VariableDefinition[],
|
|
109
108
|
parameters: Record<string, unknown>,
|
|
110
109
|
// oxlint-disable-next-line typescript/no-explicit-any
|
|
111
110
|
) => Promise<any>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { VariableDefinition } from "../../analyzeQuery/types";
|
|
2
|
+
import type { ProcedureResolver } from "../../types/db";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Orders a procedure call's arguments for the engines that bind positionally.
|
|
6
|
+
*
|
|
7
|
+
* `$1, $2, …` are numbered by one list and filled from another, so the two have
|
|
8
|
+
* to be the same list or the values land on the wrong parameters. Signature
|
|
9
|
+
* order comes from the introspected metadata; the caller's argument object is
|
|
10
|
+
* unordered and reflects the GraphQL query text.
|
|
11
|
+
*
|
|
12
|
+
* Arguments the metadata doesn't account for keep their supplied order and
|
|
13
|
+
* follow: an unnamed procedure parameter is introspected under a synthesized
|
|
14
|
+
* name that no caller can match, and dropping the value there would be worse
|
|
15
|
+
* than binding it where it was already going.
|
|
16
|
+
*/
|
|
17
|
+
export const orderProcedureArguments = (
|
|
18
|
+
sp: ProcedureResolver,
|
|
19
|
+
variables: Record<string, unknown>,
|
|
20
|
+
): VariableDefinition[] => {
|
|
21
|
+
const declared = sp.parameters.map((p) => p.name).filter((name) => name in variables);
|
|
22
|
+
const undeclared = Object.keys(variables).filter((name) => !declared.includes(name));
|
|
23
|
+
|
|
24
|
+
return [...declared, ...undeclared].map((name) => ({ name, type: "", required: false }));
|
|
25
|
+
};
|
|
@@ -161,7 +161,6 @@ export const executeQueryJSONSingle = executeQueryJSONFactory(true);
|
|
|
161
161
|
|
|
162
162
|
export const callStoredProcedure = async (
|
|
163
163
|
sp: ProcedureResolver,
|
|
164
|
-
variablesDefinition: VariableDefinition[],
|
|
165
164
|
variables: Record<string, unknown> = {},
|
|
166
165
|
) => {
|
|
167
166
|
try {
|
|
@@ -170,8 +169,6 @@ export const callStoredProcedure = async (
|
|
|
170
169
|
const request = pool!.request();
|
|
171
170
|
|
|
172
171
|
for (const [key, value] of Object.entries(variables)) {
|
|
173
|
-
if (!value) continue;
|
|
174
|
-
|
|
175
172
|
const paramFound = sp.parameters.find((p) => p.name === `@${key}`);
|
|
176
173
|
|
|
177
174
|
if (paramFound?.dataType === "varchar") {
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
VariableDefinition,
|
|
5
5
|
} from "../../../../analyzeQuery/types";
|
|
6
6
|
import type { MergedEntities } from "../../../../configuration/getSchemas/mergeEntities";
|
|
7
|
-
import type { GroupByInfo, PageLimits } from "../../../common";
|
|
7
|
+
import type { GroupByInfo, PageLimits, QueryPathFrame } from "../../../common";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
buildOrderByClauseMSSQL,
|
|
@@ -265,6 +265,7 @@ export const buildSQLForField = (
|
|
|
265
265
|
level: number,
|
|
266
266
|
aliasMap: { [alias: string]: string },
|
|
267
267
|
pageLimits: PageLimits | null,
|
|
268
|
+
ancestors: readonly QueryPathFrame[] = [],
|
|
268
269
|
): string => {
|
|
269
270
|
const tableAlias = generateTableAlias(level);
|
|
270
271
|
|
|
@@ -290,6 +291,7 @@ export const buildSQLForField = (
|
|
|
290
291
|
parentTableAlias,
|
|
291
292
|
level,
|
|
292
293
|
aliasMap,
|
|
294
|
+
ancestors,
|
|
293
295
|
);
|
|
294
296
|
|
|
295
297
|
// Check if this is a GROUP BY query
|
|
@@ -330,6 +332,7 @@ export const buildSQLForField = (
|
|
|
330
332
|
level,
|
|
331
333
|
aliasMap,
|
|
332
334
|
pageLimits,
|
|
335
|
+
[...ancestors, { table: resolverName, alias: tableAlias }],
|
|
333
336
|
),
|
|
334
337
|
([name, selector]) => `${selector} AS ${wrapIdentifierMSSQL(name)}`,
|
|
335
338
|
);
|
|
@@ -7,6 +7,7 @@ import type { ProcedureResolver } from "../../../types/db";
|
|
|
7
7
|
|
|
8
8
|
import { BunSQLConnectionOptionsZod } from "../../../config/types/db";
|
|
9
9
|
import { databasesConnections } from "../../../singletons/databases";
|
|
10
|
+
import { orderProcedureArguments } from "../../core/procedure-arguments";
|
|
10
11
|
import { toMySQLPlaceholders } from "./placeholders";
|
|
11
12
|
|
|
12
13
|
// Every pool bound comes from the schema, which is the only place they are
|
|
@@ -97,22 +98,24 @@ export const executeQueryJSONFactory =
|
|
|
97
98
|
export const executeQueryJSON = executeQueryJSONFactory();
|
|
98
99
|
export const executeQueryJSONSingle = executeQueryJSONFactory(true);
|
|
99
100
|
|
|
101
|
+
// The generated schema types every stored procedure mutation as `Boolean!`, so
|
|
102
|
+
// the call reports whether it ran, not what it selected. Returning the rows here
|
|
103
|
+
// would put an array behind a field the schema promises is a boolean.
|
|
100
104
|
export const callStoredProcedure = async (
|
|
101
105
|
sp: ProcedureResolver,
|
|
102
|
-
variablesDefinition: VariableDefinition[],
|
|
103
106
|
variables: Record<string, unknown>,
|
|
104
107
|
) => {
|
|
105
108
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
const args = orderProcedureArguments(sp, variables);
|
|
110
|
+
|
|
111
|
+
await executeQuery(
|
|
112
|
+
`CALL ${sp.dottedQuotedName}(${args.map((_, i) => `$${i + 1}`).join(", ")});`,
|
|
110
113
|
sp.db!,
|
|
111
|
-
|
|
114
|
+
args,
|
|
112
115
|
variables,
|
|
113
116
|
);
|
|
114
117
|
|
|
115
|
-
return
|
|
118
|
+
return true;
|
|
116
119
|
} catch {
|
|
117
120
|
return false;
|
|
118
121
|
}
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
VariableDefinition,
|
|
5
5
|
} from "../../../../analyzeQuery/types";
|
|
6
6
|
import type { MergedEntities } from "../../../../configuration/getSchemas/mergeEntities";
|
|
7
|
-
import type { GroupByInfo, PageLimits } from "../../../common";
|
|
7
|
+
import type { GroupByInfo, PageLimits, QueryPathFrame } from "../../../common";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
buildOrderByClauseMySQL,
|
|
@@ -105,7 +105,7 @@ const buildGroupedQuery = (
|
|
|
105
105
|
selectClauses.push(`'${agg.nameResolved}', ${cteAlias}.${agg.alias}`);
|
|
106
106
|
} else {
|
|
107
107
|
selectClauses.push(
|
|
108
|
-
`'${agg.nameResolved}', JSON_OBJECT('${agg.fieldAlias}',
|
|
108
|
+
`'${agg.nameResolved}', JSON_OBJECT('${agg.fieldAlias}', ${cteAlias}.${agg.alias})`,
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
});
|
|
@@ -257,6 +257,7 @@ export const buildSQLForField = (
|
|
|
257
257
|
level: number,
|
|
258
258
|
aliasMap: { [alias: string]: string },
|
|
259
259
|
pageLimits: PageLimits | null,
|
|
260
|
+
ancestors: readonly QueryPathFrame[] = [],
|
|
260
261
|
): string => {
|
|
261
262
|
const tableAlias = generateTableAlias(level);
|
|
262
263
|
|
|
@@ -282,6 +283,7 @@ export const buildSQLForField = (
|
|
|
282
283
|
parentTableAlias,
|
|
283
284
|
level,
|
|
284
285
|
aliasMap,
|
|
286
|
+
ancestors,
|
|
285
287
|
);
|
|
286
288
|
|
|
287
289
|
// Check if this is a GROUP BY query
|
|
@@ -322,6 +324,7 @@ export const buildSQLForField = (
|
|
|
322
324
|
level,
|
|
323
325
|
aliasMap,
|
|
324
326
|
pageLimits,
|
|
327
|
+
[...ancestors, { table: resolverName, alias: tableAlias }],
|
|
325
328
|
),
|
|
326
329
|
([name, selector]) => `'${name}', ${selector}`,
|
|
327
330
|
);
|
|
@@ -7,6 +7,7 @@ import type { ProcedureResolver } from "../../../types/db";
|
|
|
7
7
|
|
|
8
8
|
import { BunSQLConnectionOptionsZod } from "../../../config/types/db";
|
|
9
9
|
import { databasesConnections } from "../../../singletons/databases";
|
|
10
|
+
import { orderProcedureArguments } from "../../core/procedure-arguments";
|
|
10
11
|
import { getQueryTimeoutMs } from "../../../singletons/queryTimeout";
|
|
11
12
|
|
|
12
13
|
// Every pool bound comes from the schema, which is the only place they are
|
|
@@ -138,22 +139,24 @@ export const executeQueryJSONFactory =
|
|
|
138
139
|
export const executeQueryJSON = executeQueryJSONFactory();
|
|
139
140
|
export const executeQueryJSONSingle = executeQueryJSONFactory(true);
|
|
140
141
|
|
|
142
|
+
// The generated schema types every stored procedure mutation as `Boolean!`, so
|
|
143
|
+
// the call reports whether it ran, not what it selected. Returning the rows here
|
|
144
|
+
// would put an array behind a field the schema promises is a boolean.
|
|
141
145
|
export const callStoredProcedure = async (
|
|
142
146
|
sp: ProcedureResolver,
|
|
143
|
-
variablesDefinition: VariableDefinition[],
|
|
144
147
|
variables: Record<string, unknown>,
|
|
145
148
|
) => {
|
|
146
149
|
try {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
const args = orderProcedureArguments(sp, variables);
|
|
151
|
+
|
|
152
|
+
await executeQuery(
|
|
153
|
+
`SELECT * FROM ${sp.dottedQuotedName}(${args.map((_, i) => `$${i + 1}`).join(", ")});`,
|
|
151
154
|
sp.db!,
|
|
152
|
-
|
|
155
|
+
args,
|
|
153
156
|
variables,
|
|
154
157
|
);
|
|
155
158
|
|
|
156
|
-
return
|
|
159
|
+
return true;
|
|
157
160
|
} catch {
|
|
158
161
|
return false;
|
|
159
162
|
}
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
VariableDefinition,
|
|
5
5
|
} from "../../../../analyzeQuery/types";
|
|
6
6
|
import type { MergedEntities } from "../../../../configuration/getSchemas/mergeEntities";
|
|
7
|
-
import type { GroupByInfo, PageLimits } from "../../../common";
|
|
7
|
+
import type { GroupByInfo, PageLimits, QueryPathFrame } from "../../../common";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
buildOrderByClausePG,
|
|
@@ -101,7 +101,7 @@ const buildGroupedQuery = (
|
|
|
101
101
|
selectClauses.push(`'${agg.nameResolved}', ${cteAlias}.${agg.alias}`);
|
|
102
102
|
} else {
|
|
103
103
|
selectClauses.push(
|
|
104
|
-
`'${agg.nameResolved}', json_build_object('${agg.fieldAlias}',
|
|
104
|
+
`'${agg.nameResolved}', json_build_object('${agg.fieldAlias}', ${cteAlias}.${agg.alias})`,
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
});
|
|
@@ -242,6 +242,7 @@ export const buildSQLForField = (
|
|
|
242
242
|
level: number,
|
|
243
243
|
aliasMap: { [alias: string]: string },
|
|
244
244
|
pageLimits: PageLimits | null,
|
|
245
|
+
ancestors: readonly QueryPathFrame[] = [],
|
|
245
246
|
): string => {
|
|
246
247
|
const tableAlias = generateTableAlias(level);
|
|
247
248
|
|
|
@@ -267,6 +268,7 @@ export const buildSQLForField = (
|
|
|
267
268
|
parentTableAlias,
|
|
268
269
|
level,
|
|
269
270
|
aliasMap,
|
|
271
|
+
ancestors,
|
|
270
272
|
);
|
|
271
273
|
|
|
272
274
|
// Check if this is a GROUP BY query
|
|
@@ -307,6 +309,7 @@ export const buildSQLForField = (
|
|
|
307
309
|
level,
|
|
308
310
|
aliasMap,
|
|
309
311
|
pageLimits,
|
|
312
|
+
[...ancestors, { table: resolverName, alias: tableAlias }],
|
|
310
313
|
),
|
|
311
314
|
([name, selector]) => `'${name}', ${selector}`,
|
|
312
315
|
);
|