@dav3/gql-of-power 1.7.0 → 1.8.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.
@@ -507,6 +507,10 @@ class GQLtoSQLMapper {
507
507
  parentAlias: alias,
508
508
  alias: childAlias,
509
509
  parentGqlFieldNameKey: parentGqlFieldNameKey,
510
+ // Pass the inline filter (from `books(filter: {...})`) to the
511
+ // CHILD's recursiveMap so the WHERE lands inside the child's
512
+ // lateral join subquery — child-row filtering, NOT EXISTS on parent.
513
+ gqlFilters: mapping.inlineFilter ? [mapping.inlineFilter] : undefined,
510
514
  });
511
515
  variables_1.logger.log('NEW MAPPING:', { parentGqlFieldNameKey, gqlFieldName }, [...newMappings.keys()], (0, gql_to_sql_types_1.mappingsTypeToString)(mapping, true));
512
516
  const newMapping = utils_1.QueriesUtils.newMappings();
@@ -567,41 +571,28 @@ class GQLtoSQLMapper {
567
571
  const { filter, pagination } = args ?? {};
568
572
  variables_1.logger.log(prefix, 'args', parentGqlFieldNameKey, { ...filter }, JSON.stringify(pagination));
569
573
  if (filter || pagination) {
570
- // Convert mapNumericEnum string keys → raw DB values for inline field
571
- // filter args. The top-level filter goes through convertFilterEnumValues
572
- // in GQLQueryManager, but inline filter args (e.g. `books(filter: {...})`)
573
- // enter here and would otherwise bypass enum conversion, causing
574
- // mapNumericEnum fields in sub-entity filters to fail silently.
575
- //
576
- // The filter keys belong to the TARGET entity (e.g. Book), not the parent
577
- // (e.g. Author). We WRAP the filter under the field name and convert using
578
- // the PARENT entity's registries — convertRelationField /
579
- // convertMappedCustomField then resolve the target entity and recurse into
580
- // its enum fields, handling arbitrary nesting depth.
581
- const gqlEntityName = (0, gql_entity_1.getGQLEntityNameFor)(entityMetadata.name ?? '');
582
- const parentEnumFields = (0, gql_entity_1.getMapEnumFieldsFor)(gqlEntityName);
583
- const parentCustomFields = (0, gql_entity_1.getCustomFieldsFor)(gqlEntityName);
584
- const parentRelationFields = (0, gql_entity_1.getRelationFieldsFor)(gqlEntityName);
585
- const wrappedConverted = (0, enum_filter_converter_1.convertFilterEnumValues)({ [parentGqlFieldNameKey]: filter }, parentEnumFields, parentCustomFields, parentRelationFields);
586
- const convertedFilter = wrappedConverted[parentGqlFieldNameKey] ?? filter;
587
- const mapped = this.recursiveMap({
588
- entityMetadata,
589
- parentAlias: alias,
590
- alias,
591
- gqlFilters: [
592
- { [parentGqlFieldNameKey]: { ...convertedFilter } },
593
- ],
594
- isFieldFilter: true,
595
- });
596
- const { innerJoin, where: w, values, _or, _and, _not } = utils_1.QueriesUtils.mappingsReducer(mapped);
597
- mapping.innerJoin.push(...innerJoin);
598
- mapping.where.push(...w);
599
- mapping.values = { ...mapping.values, ...values };
574
+ if (filter) {
575
+ // Convert mapNumericEnum string keys → raw DB values for inline
576
+ // field filter args. The filter keys belong to the TARGET entity
577
+ // (e.g. Book), not the parent (e.g. Author). We WRAP the filter
578
+ // under the field name and convert using the PARENT entity's
579
+ // registries — convertRelationField / convertMappedCustomField
580
+ // then resolve the target entity and recurse into its enum
581
+ // fields, handling arbitrary nesting depth.
582
+ const gqlEntityName = (0, gql_entity_1.getGQLEntityNameFor)(entityMetadata.name ?? '');
583
+ const parentEnumFields = (0, gql_entity_1.getMapEnumFieldsFor)(gqlEntityName);
584
+ const parentCustomFields = (0, gql_entity_1.getCustomFieldsFor)(gqlEntityName);
585
+ const parentRelationFields = (0, gql_entity_1.getRelationFieldsFor)(gqlEntityName);
586
+ const wrappedConverted = (0, enum_filter_converter_1.convertFilterEnumValues)({ [parentGqlFieldNameKey]: filter }, parentEnumFields, parentCustomFields, parentRelationFields);
587
+ const convertedFilter = wrappedConverted[parentGqlFieldNameKey] ?? filter;
588
+ // Store the converted filter for mapField to pass to the child's
589
+ // recursiveMap. This ensures the WHERE clause lands inside the
590
+ // child's lateral join subquery (child-row filtering), NOT as an
591
+ // EXISTS on the parent.
592
+ mapping.inlineFilter = convertedFilter;
593
+ }
600
594
  mapping.limit = pagination?.limit;
601
595
  mapping.offset = pagination?.offset;
602
- mapping._or.push(..._or);
603
- mapping._and.push(..._and);
604
- mapping._not.push(..._not);
605
596
  mapping.orderBy.push(...(pagination?.orderBy ?? []));
606
597
  variables_1.logger.log('GQLtoSQLMapper - handleFieldArguments - processed', filter, 'mapping', (0, gql_to_sql_types_1.mappingsTypeToString)(mapping, true));
607
598
  }
@@ -15,6 +15,15 @@ export type MappingsType = {
15
15
  _or: MappingsType[];
16
16
  _and: MappingsType[];
17
17
  _not: MappingsType[];
18
+ /**
19
+ * Converted inline filter args from a relation field's `filter` argument
20
+ * (e.g. `books(filter: { title_eq: "X" })`). Stored by
21
+ * `handleFieldArguments` and consumed by `mapField`, which passes it to the
22
+ * child entity's `recursiveMap` as `gqlFilters` so the WHERE lands inside
23
+ * the child's lateral join subquery (child-row filtering), NOT as an EXISTS
24
+ * on the parent.
25
+ */
26
+ inlineFilter?: any;
18
27
  };
19
28
  export declare const mappingsTypeToString: (m: MappingsType, full?: boolean) => string;
20
29
  export type FilterMappingType = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dav3/gql-of-power",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Generate a single optimized SQL query from GraphQL operations — eliminate N+1 with recursive field-to-SQL mapping. Ships with agent skill for AI coding assistants.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -998,6 +998,10 @@ export class GQLtoSQLMapper {
998
998
  parentAlias: alias,
999
999
  alias: childAlias,
1000
1000
  parentGqlFieldNameKey: parentGqlFieldNameKey,
1001
+ // Pass the inline filter (from `books(filter: {...})`) to the
1002
+ // CHILD's recursiveMap so the WHERE lands inside the child's
1003
+ // lateral join subquery — child-row filtering, NOT EXISTS on parent.
1004
+ gqlFilters: mapping.inlineFilter ? [mapping.inlineFilter] : undefined,
1001
1005
  });
1002
1006
 
1003
1007
  logger.log(
@@ -1182,49 +1186,35 @@ export class GQLtoSQLMapper {
1182
1186
 
1183
1187
  logger.log(prefix, 'args', parentGqlFieldNameKey, { ...filter }, JSON.stringify(pagination));
1184
1188
  if (filter || pagination) {
1185
- // Convert mapNumericEnum string keys → raw DB values for inline field
1186
- // filter args. The top-level filter goes through convertFilterEnumValues
1187
- // in GQLQueryManager, but inline filter args (e.g. `books(filter: {...})`)
1188
- // enter here and would otherwise bypass enum conversion, causing
1189
- // mapNumericEnum fields in sub-entity filters to fail silently.
1190
- //
1191
- // The filter keys belong to the TARGET entity (e.g. Book), not the parent
1192
- // (e.g. Author). We WRAP the filter under the field name and convert using
1193
- // the PARENT entity's registries — convertRelationField /
1194
- // convertMappedCustomField then resolve the target entity and recurse into
1195
- // its enum fields, handling arbitrary nesting depth.
1196
- const gqlEntityName = getGQLEntityNameFor(entityMetadata.name ?? '');
1197
- const parentEnumFields = getMapEnumFieldsFor(gqlEntityName);
1198
- const parentCustomFields = getCustomFieldsFor(gqlEntityName);
1199
- const parentRelationFields = getRelationFieldsFor(gqlEntityName);
1200
- const wrappedConverted = convertFilterEnumValues(
1201
- { [parentGqlFieldNameKey]: filter },
1202
- parentEnumFields,
1203
- parentCustomFields,
1204
- parentRelationFields
1205
- );
1206
- const convertedFilter = wrappedConverted[parentGqlFieldNameKey] ?? filter;
1207
-
1208
- const mapped = this.recursiveMap({
1209
- entityMetadata,
1210
- parentAlias: alias,
1211
- alias,
1212
- gqlFilters: [
1213
- { [parentGqlFieldNameKey]: { ...convertedFilter } } as GQLEntityFilterInputFieldType<T>,
1214
- ],
1215
- isFieldFilter: true,
1216
- });
1189
+ if (filter) {
1190
+ // Convert mapNumericEnum string keys → raw DB values for inline
1191
+ // field filter args. The filter keys belong to the TARGET entity
1192
+ // (e.g. Book), not the parent (e.g. Author). We WRAP the filter
1193
+ // under the field name and convert using the PARENT entity's
1194
+ // registries — convertRelationField / convertMappedCustomField
1195
+ // then resolve the target entity and recurse into its enum
1196
+ // fields, handling arbitrary nesting depth.
1197
+ const gqlEntityName = getGQLEntityNameFor(entityMetadata.name ?? '');
1198
+ const parentEnumFields = getMapEnumFieldsFor(gqlEntityName);
1199
+ const parentCustomFields = getCustomFieldsFor(gqlEntityName);
1200
+ const parentRelationFields = getRelationFieldsFor(gqlEntityName);
1201
+ const wrappedConverted = convertFilterEnumValues(
1202
+ { [parentGqlFieldNameKey]: filter },
1203
+ parentEnumFields,
1204
+ parentCustomFields,
1205
+ parentRelationFields
1206
+ );
1207
+ const convertedFilter = wrappedConverted[parentGqlFieldNameKey] ?? filter;
1217
1208
 
1218
- const { innerJoin, where: w, values, _or, _and, _not } = QueriesUtils.mappingsReducer(mapped);
1209
+ // Store the converted filter for mapField to pass to the child's
1210
+ // recursiveMap. This ensures the WHERE clause lands inside the
1211
+ // child's lateral join subquery (child-row filtering), NOT as an
1212
+ // EXISTS on the parent.
1213
+ mapping.inlineFilter = convertedFilter;
1214
+ }
1219
1215
 
1220
- mapping.innerJoin.push(...innerJoin);
1221
- mapping.where.push(...w);
1222
- mapping.values = { ...mapping.values, ...values };
1223
1216
  mapping.limit = pagination?.limit;
1224
1217
  mapping.offset = pagination?.offset;
1225
- mapping._or.push(..._or);
1226
- mapping._and.push(..._and);
1227
- mapping._not.push(..._not);
1228
1218
  mapping.orderBy.push(...(pagination?.orderBy ?? []));
1229
1219
  logger.log(
1230
1220
  'GQLtoSQLMapper - handleFieldArguments - processed',
@@ -19,6 +19,15 @@ export type MappingsType = {
19
19
  _or: MappingsType[];
20
20
  _and: MappingsType[];
21
21
  _not: MappingsType[];
22
+ /**
23
+ * Converted inline filter args from a relation field's `filter` argument
24
+ * (e.g. `books(filter: { title_eq: "X" })`). Stored by
25
+ * `handleFieldArguments` and consumed by `mapField`, which passes it to the
26
+ * child entity's `recursiveMap` as `gqlFilters` so the WHERE lands inside
27
+ * the child's lateral join subquery (child-row filtering), NOT as an EXISTS
28
+ * on the parent.
29
+ */
30
+ inlineFilter?: any;
22
31
  };
23
32
 
24
33
  export const mappingsTypeToString = (m: MappingsType, full = false) => {