@dav3/gql-of-power 1.0.1 → 1.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 (40) hide show
  1. package/README.md +304 -266
  2. package/dist/entities/gql-entity.d.ts +165 -9
  3. package/dist/entities/gql-entity.js +343 -88
  4. package/dist/entities/index.js +17 -1
  5. package/dist/index.js +21 -5
  6. package/dist/operations.js +10 -4
  7. package/dist/queries/alias.js +11 -6
  8. package/dist/queries/filter-processor.d.ts +7 -4
  9. package/dist/queries/filter-processor.js +140 -130
  10. package/dist/queries/gql-to-sql-mapper.js +173 -108
  11. package/dist/queries/index.js +18 -2
  12. package/dist/queries/relationship-handler.js +33 -38
  13. package/dist/queries/sql-builder.d.ts +2 -1
  14. package/dist/queries/sql-builder.js +31 -16
  15. package/dist/queries/utils.js +7 -2
  16. package/dist/query-manager.d.ts +1 -1
  17. package/dist/query-manager.js +66 -55
  18. package/dist/types/access-control.js +5 -2
  19. package/dist/types/gql-to-sql-types.js +7 -3
  20. package/dist/types/gql-types.js +2 -1
  21. package/dist/types/index.js +20 -4
  22. package/dist/types/sql-types.d.ts +114 -7
  23. package/dist/types/sql-types.js +7 -4
  24. package/dist/types/utils.js +2 -1
  25. package/dist/utils/index.js +17 -1
  26. package/dist/utils/object.js +5 -1
  27. package/dist/variables.js +8 -4
  28. package/package.json +2 -3
  29. package/src/entities/gql-entity.ts +375 -61
  30. package/src/entities/index.ts +1 -1
  31. package/src/queries/alias.ts +5 -1
  32. package/src/queries/filter-processor.ts +118 -108
  33. package/src/queries/gql-to-sql-mapper.ts +167 -61
  34. package/src/queries/relationship-handler.ts +14 -16
  35. package/src/queries/sql-builder.ts +46 -33
  36. package/src/query-manager.ts +54 -42
  37. package/src/types/gql-types.ts +28 -26
  38. package/src/types/sql-types.ts +141 -31
  39. package/src/types/utils.ts +2 -3
  40. package/jest.config.js +0 -11
package/README.md CHANGED
@@ -10,7 +10,7 @@ In the age of microservices and data loaders, a shadow falls upon performance: _
10
10
 
11
11
  But what if there was another way? What if, with the power of a single query, you could bind all your data together?
12
12
 
13
- **GQL of Power** is a TypeScript library that harnesses ancient sorcery to generate perfectly optimized SQL queries from your GraphQL operations. It eliminates the need for data loaders entirely—one query to fetch your complete data structure, no matter how deeply nested your relationships run.
13
+ **GQL of Power** is a TypeScript library that harnesses ancient sorcery to generate perfectly optimized SQL queries from your GraphQL operations. It eliminates the need for data loaders entirely — one query to fetch your complete data structure, no matter how deeply nested your relationships run.
14
14
 
15
15
  Like the One Ring wielding dominion over all other Rings, GQL of Power commands your database with singular authority.
16
16
 
@@ -20,7 +20,7 @@ Like the One Ring wielding dominion over all other Rings, GQL of Power commands
20
20
 
21
21
  ### 🗡️ Query Unification
22
22
 
23
- Generate a **single, optimized SQL query** from any GraphQL query structure—no matter how complex your relationships are.
23
+ Generate a **single, optimized SQL query** from any GraphQL query structure — no matter how complex your relationships are.
24
24
 
25
25
  - ✨ **Eliminate Data Loaders** – Replace the tedious choreography of multiple queries with one powerful statement
26
26
  - 🏰 **Intelligent Relationship Handling** – Navigate 1:1, 1:m, m:1, and m:m relationships seamlessly
@@ -33,391 +33,429 @@ Generate a **single, optimized SQL query** from any GraphQL query structure—no
33
33
  - **Recursive Query Mapping** – Transforms GraphQL field selections into SQL recursively
34
34
  - **Automatic Join Generation** – Intelligently creates JOINs based on ORM entity relationships
35
35
  - **Dynamic Filtering** – Support for field-level and class-level filter operations (`_eq`, `_in`, `_like`, `_gt`, `_and`, `_or`, etc.)
36
- - **Pagination Support** – Native limit and offset handling through generated input types
37
- - **Custom Fields** – Extend entities with computed properties, DataLoaders, and business logic
38
- - **Framework Agnostic** – Works with any ORM through a simple MetadataProvider interface
36
+ - **Pagination & Sorting** – Native limit, offset, and order-by handling
37
+ - **Custom Fields** – Extend entities with computed properties, DataLoaders, or automatic SQL JOINs for unmapped foreign keys
38
+ - **Framework Agnostic** – Works with any ORM through a simple `MetadataProvider` interface
39
39
 
40
40
  ---
41
41
 
42
42
  ## Installation
43
43
 
44
44
  ```bash
45
- # Using pnpm (recommended)
46
45
  pnpm add @dav3/gql-of-power
47
-
48
- # Using npm
49
- npm install @dav3/gql-of-power
50
-
51
- # Using yarn
52
- yarn add @dav3/gql-of-power
53
46
  ```
54
47
 
55
48
  ---
56
49
 
57
50
  ## Quick Start
58
51
 
59
- ### 1. Define Your Entities
52
+ ### 1. Define Your GQL Entities
53
+
54
+ Use `defineFields` + `@GQLEntityClass` to declare GraphQL entities. `defineFields` is typed against the ORM class — invalid field names are caught at compile time.
60
55
 
61
56
  ```typescript
62
- import { GQLEntity } from '@dav3/gql-of-power';
63
- import { ObjectType, Field, Int } from 'type-graphql';
57
+ import { defineFields, GQLEntityBase, GQLEntityClass } from '@dav3/gql-of-power';
58
+ import { Author, Book } from './orm-entities';
59
+ import { ID, Int } from 'type-graphql';
60
+
61
+ // --- Author ---
62
+ const authorFields = defineFields(Author, {
63
+ id: { type: () => ID, generateFilter: true },
64
+ name: { type: () => String, generateFilter: true },
65
+ });
64
66
 
65
- @ObjectType()
66
- export class Author extends GQLEntity {
67
- @Field(() => Int)
68
- id: number;
67
+ @GQLEntityClass(Author, authorFields)
68
+ export class AuthorGQL extends GQLEntityBase {}
69
69
 
70
- @Field()
71
- name: string;
70
+ // --- Book ---
71
+ const bookFields = defineFields(Book, {
72
+ id: { type: () => ID, generateFilter: true },
73
+ title: { type: () => String, generateFilter: true },
74
+ publishedYear: { type: () => Int, generateFilter: true },
75
+ author: { type: () => AuthorGQL, options: { nullable: true } },
76
+ });
72
77
 
73
- @Field(() => [Book])
74
- books: Book[];
75
- }
78
+ @GQLEntityClass(Book, bookFields)
79
+ export class BookGQL extends GQLEntityBase {}
80
+ ```
76
81
 
77
- @ObjectType()
78
- export class Book extends GQLEntity {
79
- @Field(() => Int)
80
- id: number;
82
+ The decorator attaches generated statics to the class:
81
83
 
82
- @Field()
83
- title: string;
84
+ | Static | Purpose |
85
+ | --------------------------- | ----------------------------------------- |
86
+ | `BookGQL.FilterInput` | Generated filter input type |
87
+ | `BookGQL.PaginationInput` | Generated pagination input type |
88
+ | `BookGQL.OrderBy` | Generated order-by input type |
89
+ | `BookGQL.FieldsResolver` | Auto-generated field resolver class |
90
+ | `BookGQL.gqlEntityName` | GQL type name (with suffix if configured) |
91
+ | `BookGQL.relatedEntityName` | ORM entity class name (`'Book'`) |
84
92
 
85
- @Field()
86
- publicationYear: number;
93
+ ### 2. Write Resolvers
87
94
 
88
- @Field(() => Author)
89
- author: Author;
95
+ ```typescript
96
+ import { Resolver, Query, Arg, Info } from 'type-graphql';
97
+ import { GQLResolver, getAutoResolvers } from '@dav3/gql-of-power';
98
+ import { GraphQLResolveInfo } from 'graphql';
99
+
100
+ @GQLResolver(BookGQL)
101
+ export class BookResolver {
102
+ @Query(() => [BookGQL])
103
+ async books(
104
+ @Arg('filter', () => BookGQL.FilterInput, { nullable: true }) filter: any,
105
+ @Arg('pagination', () => BookGQL.PaginationInput, { nullable: true }) pagination: any,
106
+ @Info() info: GraphQLResolveInfo
107
+ ) {
108
+ return queryManager.getQueryResultsForInfo(metadataProvider, BookGQL, info, filter, pagination);
109
+ }
90
110
  }
91
111
  ```
92
112
 
93
- ### 2. Configure Metadata
94
-
95
- Provide ORM metadata that tells GQL of Power about your tables, columns, and relationships:
113
+ ### 3. Build the Schema
96
114
 
97
115
  ```typescript
98
- const metadataProvider = {
99
- getEntityMetadata: (entity) => ({
100
- tableName: 'authors',
101
- primaryKey: 'id',
102
- fields: {
103
- id: { columnName: 'id' },
104
- name: { columnName: 'name' },
105
- },
106
- }),
107
- // ... implement for all entities
108
- };
116
+ import { buildSchemaSync } from 'type-graphql';
117
+ import { getAutoResolvers } from '@dav3/gql-of-power';
118
+ import '../entities'; // trigger @GQLEntityClass decoration
119
+
120
+ const schema = buildSchemaSync({
121
+ resolvers: [
122
+ BookResolver,
123
+ ...getAutoResolvers(), // FieldsResolver for every @GQLEntityClass entity
124
+ ],
125
+ });
109
126
  ```
110
127
 
111
- ### 3. Execute Your Query
128
+ ---
129
+
130
+ ## Examples
131
+
132
+ For complete, LotR-themed walkthroughs of both mapping strategies (direct ORM relation and custom field mapping), see **[EXAMPLES.md](./EXAMPLES.md)**.
133
+
134
+ ---
135
+
136
+ ## Entity API
137
+
138
+ ### `defineFields<T>(OrmClass, config)`
139
+
140
+ Typed wrapper that constrains config keys to `keyof T`. Identity function at runtime — it exists purely for TypeScript inference.
112
141
 
113
142
  ```typescript
114
- import { QueryManager } from '@dav3/gql-of-power';
143
+ const fields = defineFields(Book, {
144
+ id: { type: () => ID, generateFilter: true },
145
+ title: { type: () => String, generateFilter: true },
146
+ typo: { type: () => String }, // TS error — 'typo' is not keyof Book ✗
147
+ });
148
+ ```
115
149
 
116
- const queryManager = new QueryManager(metadataProvider, knexInstance);
150
+ #### Field options
117
151
 
118
- // Execute a complex query with a single SQL statement
119
- const result = await queryManager.execute(
120
- Author,
121
- gqlInfo, // GraphQL resolve info
122
- {
123
- filter: { name: { _eq: 'Tolkien' } },
124
- pagination: { limit: 10, offset: 0 },
125
- }
126
- );
152
+ | Option | Type | Purpose |
153
+ | ------------------- | ------------------- | ------------------------------------------------------------------------------------- |
154
+ | `type` | `() => GraphQLType` | GraphQL return type (required) |
155
+ | `generateFilter` | `boolean` | Generate filter input fields for this property |
156
+ | `options` | `FieldOptions` | type-graphql field options (nullable, description, etc.) |
157
+ | `alias` | `string` | Override the GQL field name |
158
+ | `array` | `true` | Mark as array return type |
159
+ | `relatedEntityName` | `() => string` | ORM entity name for array relation fields (auto-derived when using `@GQLEntityClass`) |
160
+ | `enum` | `EnumData` | Register an enum type |
127
161
 
128
- // Returns all authors with all their books, published authors, and nested relationships
162
+ ### `@GQLEntityClass(OrmClass, fields, extra?)`
163
+
164
+ Class decorator that registers the entity with type-graphql and attaches generated statics.
165
+
166
+ ```typescript
167
+ @GQLEntityClass(Book, fields, {
168
+ customFields: { ... }, // optional — see Custom Fields
169
+ acl: { ... }, // optional — access control
170
+ })
171
+ export class BookGQL extends GQLEntityBase {}
129
172
  ```
130
173
 
131
- ---
174
+ ### `extends GQLEntityBase`
132
175
 
133
- ## Architecture: The Rings of Power
176
+ Required base class. Provides TypeScript visibility of the decorator-attached statics (`FilterInput`, `PaginationInput`, etc.) without `declare static` boilerplate on each entity.
134
177
 
135
- ### The Fellowship: Core Components
178
+ ### `@GQLResolver(EntityClass)`
136
179
 
137
- | Component | Purpose |
138
- | ------------------ | ----------------------------------------------------------------- |
139
- | **GQLtoSQLMapper** | The main artifact—transforms GraphQL into SQL with proper joins |
140
- | **QueryManager** | The orchestrator—manages the entire query execution flow |
141
- | **EntitySystem** | The registry—defines GraphQL entities with auto-generated filters |
142
- | **Operations** | The spells—defines all available filtering operations |
180
+ Marks a class as a custom resolver for the given GQL entity. Applies `@Resolver(() => EntityClass)` and integrates with type-graphql's resolver merging (the auto `FieldsResolver` and your custom query resolver coexist seamlessly).
143
181
 
144
- ### The Fellowship's Journey: Query Flow
182
+ ### `getAutoResolvers()`
145
183
 
146
- 1. **Parsing** – GraphQL field selections are parsed using `graphql-fields`
147
- 2. **Mapping** – `GQLtoSQLMapper.recursiveMap()` builds SQL recursively
148
- 3. **Aggregation** – JSON aggregation combines nested results efficiently
149
- 4. **Execution** – Final SQL is executed against your database
184
+ Returns all `FieldsResolver` classes registered by `@GQLEntityClass` decorators. Pass the result into `buildSchemaSync({ resolvers: [...getAutoResolvers()] })`.
150
185
 
151
186
  ---
152
187
 
153
- ## Advanced Usage
188
+ ## Custom Fields
189
+
190
+ Custom fields extend a GQL entity with fields that don't exist as direct ORM properties. There are two mutually exclusive strategies.
154
191
 
155
- ### Comprehensive Filtering
192
+ ### Strategy 1: `resolve` — DataLoader / computed
156
193
 
157
- Filter at any nesting level with powerful operations:
194
+ Provide a GraphQL `@FieldResolver` function. The library fetches field(s) listed in `requires` from the main query, then your `resolve` function runs at GraphQL resolution time.
158
195
 
159
196
  ```typescript
160
- // Find authors with published books
161
- await queryManager.execute(Author, gqlInfo, {
162
- filter: {
163
- books: {
164
- publicationYear: { _gte: 2000 },
197
+ @GQLEntityClass(Fellowship, fields, {
198
+ customFields: {
199
+ firstMember: {
200
+ type: () => GraphQLJSON,
201
+ options: { nullable: true },
202
+ requires: 'id', // ensure 'id' is fetched even if client didn't request it
203
+ resolveDecorators: [Root(), Ctx()],
204
+ resolve: (root: Fellowship, ctx: any) => {
205
+ return memberDataLoader.load(root.id);
206
+ },
165
207
  },
166
208
  },
167
- });
168
-
169
- // Complex nested AND/OR conditions
170
- await queryManager.execute(Author, gqlInfo, {
171
- filter: {
172
- _and: [{ name: { _like: '%Tolkien%' } }, { books: { publicationYear: { _gte: 1950 } } }],
173
- },
174
- });
209
+ })
210
+ export class FellowshipGQL extends GQLEntityBase {}
175
211
  ```
176
212
 
177
- ### Pagination & Sorting
213
+ #### `resolve` field options
178
214
 
179
- ```typescript
180
- await queryManager.execute(Author, gqlInfo, {
181
- pagination: {
182
- limit: 20,
183
- offset: 40,
184
- orderBy: 'name', // ASC by default
185
- orderByDirection: 'DESC',
186
- },
187
- });
188
- ```
189
-
190
- ### Filter Operations
215
+ | Option | Purpose |
216
+ | ------------------- | ---------------------------------------------------------------------- |
217
+ | `type` | GraphQL return type |
218
+ | `options` | type-graphql field options |
219
+ | `requires` | Field name(s) to ensure are fetched from DB |
220
+ | `resolve` | The resolver function (required) |
221
+ | `resolveDecorators` | type-graphql parameter decorators in order (`[Root(), Ctx(), Info()]`) |
191
222
 
192
- | Operation | Example | Meaning |
193
- | -------------- | --------------------------------- | ---------------------------------- |
194
- | `_eq` | `{ id: { _eq: 5 } }` | Equal to |
195
- | `_ne` | `{ status: { _ne: 'inactive' } }` | Not equal to |
196
- | `_in` | `{ id: { _in: [1, 2, 3] } }` | In array |
197
- | `_like` | `{ name: { _like: '%Frodo%' } }` | String contains (case-insensitive) |
198
- | `_gt` / `_gte` | `{ year: { _gte: 1900 } }` | Greater than (or equal) |
199
- | `_lt` / `_lte` | `{ year: { _lte: 2000 } }` | Less than (or equal) |
223
+ ### Strategy 2: `mapping` — automatic SQL JOIN
200
224
 
201
- ### Custom Fields
225
+ Provide a `FieldMappingConfig`. The library generates a SQL `LEFT JOIN LATERAL` automatically and returns the related object directly from the SQL result. No resolver function needed.
202
226
 
203
- Beyond GQL of Power's automated query generation, you can add **custom fields** that execute custom logic alongside your data fetching. Perfect for computed properties, cross-cutting concerns, or batch loading.
227
+ Use this when the foreign key exists as a plain column on the entity (not declared as an ORM relation).
204
228
 
205
229
  ```typescript
206
- // Define custom fields when creating GraphQL types
207
- const FellowshipGQL = createGQLTypes(Fellowship, FellowshipFields, {
208
- customFields: {
209
- // Custom field that fetches the first fellowship member
210
- firstMember: {
211
- type: () => GraphQLJSON,
212
- options: { nullable: true },
213
- requires: ['id'], // Fields that must be fetched from the database for this resolver
214
- resolveDecorators: [Root(), Ctx(), Info()], // TypeGraphQL parameter decorators for the resolve function
215
- resolve: (root: Fellowship, ctx: any, info: any) => {
216
- // Your custom logic—can use DataLoader for batch loading
217
- return memberDataLoader.load(root.id);
218
- },
219
- },
220
- },
221
- });
222
-
223
- // Export FieldsResolver to use in your resolver classes
224
- export const FellowshipFieldsResolver = FellowshipGQL.FieldsResolver;
230
+ import { Kingdom } from './orm-entities';
231
+
232
+ @GQLEntityClass(Hobbit, fields, {
233
+ customFields: {
234
+ kingdom: {
235
+ type: () => KingdomGQL,
236
+ options: { nullable: true },
237
+ mapping: {
238
+ refEntity: Kingdom, // ORM entity class to JOIN to
239
+ refFields: 'id', // column(s) on Kingdom — keyof Kingdom ✓
240
+ fields: 'kingdomId', // column(s) on Hobbit — keyof Hobbit ✓
241
+ },
242
+ },
243
+ },
244
+ })
245
+ export class HobbitGQL extends GQLEntityBase {}
225
246
  ```
226
247
 
227
- #### Custom Field Configuration
228
-
229
- | Property | Purpose | Connection |
230
- |----------|---------|------------|
231
- | `type` | Declares the GraphQL return type that appears in the schema | The type clients see when querying this field |
232
- | `options` | TypeGraphQL field options like nullable, deprecation, descriptions | Controls field behavior in the GraphQL schema |
233
- | `requires` | Specifies which fields from the main query must be present in the result | GQL of Power ensures these database columns are fetched alongside the optimized query |
234
- | `resolve` | Function that receives the root entity object and returns the custom field value | Executed after the main query completes, receiving the fetched entity data |
235
- | `resolveDecorators` | TypeGraphQL decorators that map function parameters | Determines which context parameters (`Root`, `Ctx`, `Info`) are injected into the resolve function |
236
-
237
- #### Use Cases
248
+ Composite FK — use arrays (must have the same length):
238
249
 
239
- 🧙 **DataLoader Integration** – Batch-load related data without N+1
240
250
  ```typescript
241
- firstMember: {
242
- type: () => PersonGQL.GQLEntity,
243
- resolve: (root: Fellowship) => memberDataLoader.load(root.id),
251
+ mapping: {
252
+ refEntity: Weapon,
253
+ refFields: ['realmId', 'weaponCode'],
254
+ fields: ['realmId', 'weaponExternalId'],
244
255
  }
245
256
  ```
246
257
 
247
- ✨ **Computed Properties** – Calculate derived values
248
- ```typescript
249
- memberCount: {
250
- type: () => Number,
251
- resolve: (root: Fellowship) => root.members?.length || 0,
252
- }
258
+ #### `mapping` field options
259
+
260
+ | Option | Type | Purpose |
261
+ | ------------------- | --------------------------------- | -------------------------------------------------------------- |
262
+ | `type` | `() => GraphQLType` | GraphQL return type |
263
+ | `options` | `FieldOptions` | type-graphql field options (nullable, etc.) |
264
+ | `mapping.refEntity` | `new () => TRef` | ORM entity class to JOIN to (must be in the metadata provider) |
265
+ | `mapping.refFields` | `keyof TRef \| Array<keyof TRef>` | Column(s) on the ref entity to match against |
266
+ | `mapping.fields` | `keyof T \| Array<keyof T>` | Column(s) on the owner entity to match from |
267
+
268
+ > **Note**: `resolve` and `mapping` are mutually exclusive — TypeScript enforces this via a discriminated union. `resolveDecorators` and `requires` are only valid on the `resolve` branch.
269
+
270
+ #### Generated SQL
271
+
272
+ For `kingdom: { mapping: { refEntity: Kingdom, refFields: 'id', fields: 'kingdomId' } }`:
273
+
274
+ ```sql
275
+ select e_a1.id, e_a1.kingdom_id, f_j1.value as "kingdom"
276
+ from (
277
+ select e_a1.id, e_a1.kingdom_id
278
+ from hobbit as e_a1
279
+ where true
280
+ ) as e_a1
281
+ left outer join lateral (
282
+ select row_to_json(f_j1)::jsonb as value
283
+ from (
284
+ select f_j1.id, f_j1.name
285
+ from kingdom as f_j1
286
+ where e_a1.kingdom_id = f_j1.id
287
+ ) as f_j1
288
+ ) as f_j1 on true
253
289
  ```
254
290
 
255
- 🎭 **Business Logic** – Apply formatting, ACL, or transformations
256
- ```typescript
257
- formattedName: {
258
- type: () => String,
259
- resolve: (root: Fellowship) => formatTitle(root.name),
260
- }
261
- ```
291
+ Returns `null` when the FK column is null (LEFT JOIN).
292
+
293
+ ---
262
294
 
263
- #### Using Custom Fields in Resolvers
295
+ ## Filtering
264
296
 
265
- To make custom fields available in your GraphQL resolvers, extend the generated `FieldsResolver` class:
297
+ Filter at any nesting level:
266
298
 
267
299
  ```typescript
268
- import { Resolver, Query, Arg, Info } from 'type-graphql';
269
- import { FellowshipGQL, FellowshipFieldsResolver } from './entities';
270
-
271
- @Resolver(() => FellowshipGQL.GQLEntity)
272
- export class FellowshipResolver extends FellowshipFieldsResolver {
273
- @Query(() => [FellowshipGQL.GQLEntity])
274
- async fellowships(
275
- @Info() info: GraphQLResolveInfo,
276
- @Arg('filter', () => FellowshipGQL.GQLEntityFilterInput, { nullable: true })
277
- filter?: GQLEntityFilterInputFieldType<Fellowship>
278
- ) {
279
- return await queryManager.getQueryResultsForInfo(
280
- metadataProvider,
281
- Fellowship,
282
- info,
283
- filter
284
- );
285
- }
300
+ // Simple equality
301
+ filter: { title: 'The Fellowship of the Ring' }
302
+
303
+ // Operators
304
+ filter: { publishedYear: { _gte: 1950 } }
305
+
306
+ // Nested relation filter
307
+ filter: { author: { name: { _like: '%Tolkien%' } } }
308
+
309
+ // AND / OR
310
+ filter: {
311
+ _or: [
312
+ { title: { _like: '%Ring%' } },
313
+ { publishedYear: { _lt: 1960 } },
314
+ ],
286
315
  }
287
316
  ```
288
317
 
289
- The `FieldsResolver` automatically handles custom field resolution alongside the main query execution. When a client requests a custom field, it will be resolved using your custom resolver logic while the rest of the query is handled by GQL of Power's optimized SQL generation.
318
+ ### Filter operations
290
319
 
291
- ---
320
+ | Operation | Meaning |
321
+ | -------------- | ------------------------------------ |
322
+ | `_eq` | Equal |
323
+ | `_ne` | Not equal |
324
+ | `_in` | In array |
325
+ | `_nin` | Not in array |
326
+ | `_like` | ILIKE (case-insensitive contains) |
327
+ | `_gt` / `_gte` | Greater than / greater than or equal |
328
+ | `_lt` / `_lte` | Less than / less than or equal |
329
+ | `_and` | Logical AND |
330
+ | `_or` | Logical OR (generates UNION ALL) |
292
331
 
293
- ## Configuration
294
-
295
- ### Environment Variables
332
+ ---
296
333
 
297
- ```bash
298
- # Toggle between JSONB and string concatenation for JSON aggregation
299
- D3GOP_USE_STRING_FOR_JSONB=true
334
+ ## Pagination
300
335
 
301
- # Control logging output
302
- D3GOP_LOG_TYPE="debug" # Options: debug, disabled
336
+ ```typescript
337
+ pagination: {
338
+ limit: 20,
339
+ offset: 40,
340
+ orderBy: [{ publishedYear: 'desc' }],
341
+ }
303
342
  ```
304
343
 
305
344
  ---
306
345
 
307
- ## Common Commands
346
+ ## Relationship Handling
308
347
 
309
- ### Development
348
+ | Relationship | SQL Strategy |
349
+ | ---------------------- | ------------------------------------------------- |
350
+ | **m:1** (many-to-one) | `LEFT JOIN LATERAL` + `row_to_json` |
351
+ | **1:1** (one-to-one) | `LEFT JOIN LATERAL` + `row_to_json` |
352
+ | **1:m** (one-to-many) | `LEFT JOIN LATERAL` + `json_agg` |
353
+ | **m:m** (many-to-many) | Pivot table subquery + `json_agg` |
354
+ | **custom `mapping`** | `LEFT JOIN LATERAL` + `row_to_json` (same as m:1) |
310
355
 
311
- ```bash
312
- # Install dependencies
313
- pnpm install
356
+ ---
314
357
 
315
- # Build the library
316
- pnpm run build
358
+ ## Configuration
317
359
 
318
- # Watch mode for development
319
- pnpm run watch
360
+ ### Environment Variables
320
361
 
321
- # Run all tests
322
- pnpm test
362
+ | Variable | Purpose |
363
+ | ---------------------------- | ------------------------------------------------------------------------------------------- |
364
+ | `D3GOP_TYPES_SUFFIX` | Suffix appended to all generated GQL entity type names (e.g. `'V2'` → `BookV2`, `AuthorV2`) |
365
+ | `D3GOP_SORT_SUFFIX` | Suffix appended to sort/pagination types only (e.g. `'V2'` → `SortV2`, `BookV2OrderBy`) |
366
+ | `D3GOP_LOG_TYPE` | Logging level: `debug` or `disabled` |
367
+ | `D3GOP_DEFAULT_QUERY_LIMIT` | Default query limit when pagination is not specified (default: `3000`) |
368
+ | `D3GOP_USE_STRING_FOR_JSONB` | Toggle between JSONB and string concatenation for JSON aggregation |
323
369
 
324
- # Run unit tests only
325
- pnpm test:unit
370
+ > **Type name collision**: If you have both v1 (`createGQLTypes`) and v2 (`@GQLEntityClass`) entities in the same schema, set `D3GOP_TYPES_SUFFIX` so v2 entity names are distinct (e.g. `Hobbit` → `HobbitV2`). Use `D3GOP_SORT_SUFFIX` separately if sort/pagination types also need a suffix. No `setGlobalConfig()` call is required — the env vars are read automatically.
326
371
 
327
- # Run integration tests only
328
- pnpm test:integration
372
+ ### Programmatic config
329
373
 
330
- # Watch mode for tests
331
- pnpm test:watch
374
+ ```typescript
375
+ import { setGlobalConfig } from '@dav3/gql-of-power';
332
376
 
333
- # Generate coverage report
334
- pnpm test:coverage
377
+ // Call before any @GQLEntityClass decorators run (i.e. before importing entity files)
378
+ setGlobalConfig({ gqlTypesSuffix: 'V2' });
335
379
 
336
- # Build for publishing
337
- pnpm run prepublishOnly
380
+ // Optionally set a separate suffix for sort/pagination types
381
+ setGlobalConfig({ gqlTypesSuffix: 'V2', gqlSortSuffix: 'V2' });
338
382
  ```
339
383
 
340
384
  ---
341
385
 
342
- ## Why GQL of Power?
343
-
344
- ### The Problem It Solves
345
-
346
- Traditional GraphQL + ORM combinations suffer from the **N+1 query problem**:
386
+ ## `GQLQueryManager`
347
387
 
348
- - Query 1: Fetch 10 authors
349
- - Query 2-11: Fetch books for each author (10 queries)
350
- - Query 12-21: Fetch publication details for each book (10 queries)
351
- - **Total: 21 queries** ❌
352
-
353
- ### The Solution
388
+ ```typescript
389
+ const queryManager = new GQLQueryManager();
390
+
391
+ // From a GraphQL resolver — fields are parsed from resolve info automatically
392
+ const results = await queryManager.getQueryResultsForInfo(
393
+ metadataProvider,
394
+ BookGQL, // @GQLEntityClass-decorated class or plain ORM class
395
+ info, // GraphQLResolveInfo
396
+ filter,
397
+ pagination
398
+ );
354
399
 
355
- ```
356
- GQL of Power: Fetch all data in **1 query** ✅
400
+ // With explicit field selection (useful for testing or non-resolver contexts)
401
+ const results = await queryManager.getQueryResultsForFields(
402
+ metadataProvider,
403
+ BookGQL,
404
+ { id: {}, title: {} },
405
+ filter,
406
+ pagination
407
+ );
357
408
  ```
358
409
 
359
- By analyzing your GraphQL query structure and ORM relationships, GQL of Power generates a single, optimized SQL query with intelligent joins and aggregations that returns your complete data structure.
410
+ `BookGQL.relatedEntityName` (`'Book'`) is used automatically to look up ORM metadata — no need to pass the ORM class separately.
360
411
 
361
412
  ---
362
413
 
363
- ## Architecture Highlights
414
+ ## Architecture
364
415
 
365
- ### Relationship Handling
416
+ ### Core Components
366
417
 
367
- | Relationship | Strategy |
368
- | ------------ | --------------------------------------- |
369
- | **1:1** | Simple INNER/LEFT JOIN |
370
- | **m:1** | INNER/LEFT JOIN to parent table |
371
- | **1:m** | Subquery with JSON aggregation |
372
- | **m:m** | Through junction table with aggregation |
418
+ | Component | Purpose |
419
+ | --------------------- | -------------------------------------------------------------- |
420
+ | `GQLtoSQLMapper` | Transforms GraphQL field selections into SQL with proper joins |
421
+ | `GQLQueryManager` | Orchestrates query building and execution |
422
+ | `FilterProcessor` | Translates GQL filter inputs to SQL WHERE clauses |
423
+ | `RelationshipHandler` | Generates JOIN SQL for ORM-declared relations |
424
+ | `SQLBuilder` | Assembles final SQL strings and JSON aggregations |
425
+ | `AliasManager` | Manages incremental SQL aliases to prevent naming conflicts |
373
426
 
374
- ### Performance Optimizations
427
+ ### Query Flow
375
428
 
376
- - **Alias Management** – Incremental aliases (`a1`, `a2`, ...) prevent naming conflicts
377
- - **Set-Based Field Selection** – Eliminates duplicate fields automatically
378
- - **OR to UNION ALL** – Converts OR conditions to UNION queries for optimal execution plans
379
- - **JSONB Aggregation** – Efficient nested data structure building
429
+ 1. **Parse** – `graphql-parse-resolve-info` extracts the requested fields from `GraphQLResolveInfo`
430
+ 2. **Map** – `GQLtoSQLMapper.recursiveMap()` walks the field tree, building `select`, `join`, `where` sets
431
+ 3. **Aggregate** – JSON aggregation (`row_to_json`, `json_agg`) combines nested results
432
+ 4. **Bind** – Named parameters are bound via knex raw
433
+ 5. **Execute** – Single SQL sent to the database
380
434
 
381
435
  ---
382
436
 
383
437
  ## Known Limitations
384
438
 
385
- These dragons still slumber in the cave—future challenges for brave developers:
386
-
387
- - ⚠️ Class-level `NOT` conditions not yet supported
388
- - ⚠️ Order by reference table columns (e.g., "order authors by their latest book publication")
389
- - ⚠️ Access Control Lists (ACL) pending async refactoring
390
-
391
- ---
392
-
393
- ## Examples
394
-
395
- See the `/examples` directory for complete, runnable examples:
396
-
397
- - **Middle-earth Schema** – Full Author/Book relationship example inspired by Tolkien's lore
398
- - **Database Integration** – Real PostgreSQL examples with TypeTypeORM metadata
439
+ - ⚠️ Class-level `_not` conditions not yet supported
440
+ - ⚠️ Order by columns on related/joined tables not supported
441
+ - ⚠️ ACL pending async refactoring
399
442
 
400
443
  ---
401
444
 
402
- ## Contributing
445
+ ## Development
403
446
 
404
- Contributions are welcome! Whether you're fixing bugs, improving performance, or adding features, please submit a pull request.
447
+ ```bash
448
+ bun install # Install dependencies
449
+ bun run build # Compile TypeScript → dist/
450
+ bun run test # Run all tests
451
+ bun run test:watch # Watch mode
452
+ ```
405
453
 
406
454
  ---
407
455
 
408
456
  ## License
409
457
 
410
- MIT NON-AI License
411
-
412
- Forged by [Dav3](https://github.com/dave-palt) with the wisdom of Middle-earth.
413
-
414
- ---
415
-
416
- ## Support & Community
417
-
418
- - 📖 Full documentation coming soon
419
- - 🐛 Found a bug? [Open an issue](https://github.com/dave-palt/gql-of-power/issues)
420
- - 💬 Have a question? Reach out to the fellowship
458
+ MIT NON-AI License — Forged by [Dav3](https://github.com/dave-palt) with the wisdom of Middle-earth.
421
459
 
422
460
  ---
423
461