@dav3/gql-of-power 1.0.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/.prettierrc +7 -0
- package/LICENSE +26 -0
- package/README.md +426 -0
- package/bunfig.toml +17 -0
- package/dist/src/entities/gql-entity.d.ts +39 -0
- package/dist/src/entities/gql-entity.js +381 -0
- package/dist/src/entities/index.d.ts +1 -0
- package/dist/src/entities/index.js +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -0
- package/dist/src/operations.d.ts +116 -0
- package/dist/src/operations.js +81 -0
- package/dist/src/queries/alias.d.ts +25 -0
- package/dist/src/queries/alias.js +66 -0
- package/dist/src/queries/filter-processor.d.ts +141 -0
- package/dist/src/queries/filter-processor.js +574 -0
- package/dist/src/queries/gql-to-sql-mapper.d.ts +44 -0
- package/dist/src/queries/gql-to-sql-mapper.js +309 -0
- package/dist/src/queries/index.d.ts +2 -0
- package/dist/src/queries/index.js +2 -0
- package/dist/src/queries/relationship-handler.d.ts +19 -0
- package/dist/src/queries/relationship-handler.js +144 -0
- package/dist/src/queries/sql-builder.d.ts +115 -0
- package/dist/src/queries/sql-builder.js +179 -0
- package/dist/src/queries/utils.d.ts +7 -0
- package/dist/src/queries/utils.js +58 -0
- package/dist/src/query-manager.d.ts +18 -0
- package/dist/src/query-manager.js +99 -0
- package/dist/src/types/access-control.d.ts +27 -0
- package/dist/src/types/access-control.js +7 -0
- package/dist/src/types/gql-to-sql-types.d.ts +27 -0
- package/dist/src/types/gql-to-sql-types.js +18 -0
- package/dist/src/types/gql-types.d.ts +86 -0
- package/dist/src/types/gql-types.js +102 -0
- package/dist/src/types/index.d.ts +4 -0
- package/dist/src/types/index.js +4 -0
- package/dist/src/types/sql-types.d.ts +135 -0
- package/dist/src/types/sql-types.js +12 -0
- package/dist/src/types/utils.d.ts +12 -0
- package/dist/src/types/utils.js +1 -0
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/index.js +1 -0
- package/dist/src/utils/object.d.ts +1 -0
- package/dist/src/utils/object.js +1 -0
- package/dist/src/variables.d.ts +2 -0
- package/dist/src/variables.js +77 -0
- package/jest.config.js +11 -0
- package/package.json +48 -0
- package/scripts/update-version.ts +19 -0
- package/src/entities/gql-entity.ts +472 -0
- package/src/entities/index.ts +1 -0
- package/src/index.ts +5 -0
- package/src/operations.ts +131 -0
- package/src/queries/alias.ts +70 -0
- package/src/queries/filter-processor.ts +1156 -0
- package/src/queries/gql-to-sql-mapper.ts +644 -0
- package/src/queries/index.ts +2 -0
- package/src/queries/relationship-handler.ts +354 -0
- package/src/queries/sql-builder.ts +282 -0
- package/src/queries/utils.ts +95 -0
- package/src/query-manager.ts +129 -0
- package/src/types/access-control.ts +35 -0
- package/src/types/gql-to-sql-types.ts +51 -0
- package/src/types/gql-types.ts +251 -0
- package/src/types/index.ts +4 -0
- package/src/types/sql-types.ts +204 -0
- package/src/types/utils.ts +27 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/object.ts +2 -0
- package/src/variables.ts +81 -0
package/.prettierrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT NON-AI License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, <copyright holders>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of the software and associated documentation files (the "Software"),
|
|
6
|
+
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.
|
|
8
|
+
|
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
In addition, the following restrictions apply:
|
|
12
|
+
|
|
13
|
+
1. The Software and any modifications made to it may not be used for the purpose of training or improving machine learning algorithms,
|
|
14
|
+
including but not limited to artificial intelligence, natural language processing, or data mining. This condition applies to any derivatives,
|
|
15
|
+
modifications, or updates based on the Software code. Any usage of the Software in an AI-training dataset is considered a breach of this License.
|
|
16
|
+
|
|
17
|
+
2. The Software may not be included in any dataset used for training or improving machine learning algorithms,
|
|
18
|
+
including but not limited to artificial intelligence, natural language processing, or data mining.
|
|
19
|
+
|
|
20
|
+
3. Any person or organization found to be in violation of these restrictions will be subject to legal action and may be held liable
|
|
21
|
+
for any damages resulting from such use.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
25
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
26
|
+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# ⚔️ GQL of Power
|
|
2
|
+
|
|
3
|
+
> **"One Query to Rule Them All"** — Master your data with the power of a single GraphQL query
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Ring Bearer's Tale
|
|
8
|
+
|
|
9
|
+
In the age of microservices and data loaders, a shadow falls upon performance: _the N+1 query problem_. Weary developers traverse the lands of Middleware-earth, orchestrating countless queries to fetch interconnected data.
|
|
10
|
+
|
|
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
|
+
|
|
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
|
+
|
|
15
|
+
Like the One Ring wielding dominion over all other Rings, GQL of Power commands your database with singular authority.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
### 🗡️ Query Unification
|
|
22
|
+
|
|
23
|
+
Generate a **single, optimized SQL query** from any GraphQL query structure—no matter how complex your relationships are.
|
|
24
|
+
|
|
25
|
+
- ✨ **Eliminate Data Loaders** – Replace the tedious choreography of multiple queries with one powerful statement
|
|
26
|
+
- 🏰 **Intelligent Relationship Handling** – Navigate 1:1, 1:m, m:1, and m:m relationships seamlessly
|
|
27
|
+
- 🔍 **Comprehensive Filtering** – Filter at any level of your nested query tree
|
|
28
|
+
- 📄 **Type-Safe Operations** – Full TypeScript support ensures your queries are error-free
|
|
29
|
+
- ⚡ **Performance Optimized** – UNION ALL for OR conditions, efficient JSONB aggregation, and strategic aliasing
|
|
30
|
+
|
|
31
|
+
### 🧙 Core Powers
|
|
32
|
+
|
|
33
|
+
- **Recursive Query Mapping** – Transforms GraphQL field selections into SQL recursively
|
|
34
|
+
- **Automatic Join Generation** – Intelligently creates JOINs based on ORM entity relationships
|
|
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
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Using pnpm (recommended)
|
|
46
|
+
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
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### 1. Define Your Entities
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { GQLEntity } from '@dav3/gql-of-power';
|
|
63
|
+
import { ObjectType, Field, Int } from 'type-graphql';
|
|
64
|
+
|
|
65
|
+
@ObjectType()
|
|
66
|
+
export class Author extends GQLEntity {
|
|
67
|
+
@Field(() => Int)
|
|
68
|
+
id: number;
|
|
69
|
+
|
|
70
|
+
@Field()
|
|
71
|
+
name: string;
|
|
72
|
+
|
|
73
|
+
@Field(() => [Book])
|
|
74
|
+
books: Book[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@ObjectType()
|
|
78
|
+
export class Book extends GQLEntity {
|
|
79
|
+
@Field(() => Int)
|
|
80
|
+
id: number;
|
|
81
|
+
|
|
82
|
+
@Field()
|
|
83
|
+
title: string;
|
|
84
|
+
|
|
85
|
+
@Field()
|
|
86
|
+
publicationYear: number;
|
|
87
|
+
|
|
88
|
+
@Field(() => Author)
|
|
89
|
+
author: Author;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. Configure Metadata
|
|
94
|
+
|
|
95
|
+
Provide ORM metadata that tells GQL of Power about your tables, columns, and relationships:
|
|
96
|
+
|
|
97
|
+
```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
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3. Execute Your Query
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { QueryManager } from '@dav3/gql-of-power';
|
|
115
|
+
|
|
116
|
+
const queryManager = new QueryManager(metadataProvider, knexInstance);
|
|
117
|
+
|
|
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
|
+
);
|
|
127
|
+
|
|
128
|
+
// Returns all authors with all their books, published authors, and nested relationships
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Architecture: The Rings of Power
|
|
134
|
+
|
|
135
|
+
### The Fellowship: Core Components
|
|
136
|
+
|
|
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 |
|
|
143
|
+
|
|
144
|
+
### The Fellowship's Journey: Query Flow
|
|
145
|
+
|
|
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
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Advanced Usage
|
|
154
|
+
|
|
155
|
+
### Comprehensive Filtering
|
|
156
|
+
|
|
157
|
+
Filter at any nesting level with powerful operations:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// Find authors with published books
|
|
161
|
+
await queryManager.execute(Author, gqlInfo, {
|
|
162
|
+
filter: {
|
|
163
|
+
books: {
|
|
164
|
+
publicationYear: { _gte: 2000 },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
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
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Pagination & Sorting
|
|
178
|
+
|
|
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
|
|
191
|
+
|
|
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) |
|
|
200
|
+
|
|
201
|
+
### Custom Fields
|
|
202
|
+
|
|
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.
|
|
204
|
+
|
|
205
|
+
```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;
|
|
225
|
+
```
|
|
226
|
+
|
|
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
|
|
238
|
+
|
|
239
|
+
🧙 **DataLoader Integration** – Batch-load related data without N+1
|
|
240
|
+
```typescript
|
|
241
|
+
firstMember: {
|
|
242
|
+
type: () => PersonGQL.GQLEntity,
|
|
243
|
+
resolve: (root: Fellowship) => memberDataLoader.load(root.id),
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
✨ **Computed Properties** – Calculate derived values
|
|
248
|
+
```typescript
|
|
249
|
+
memberCount: {
|
|
250
|
+
type: () => Number,
|
|
251
|
+
resolve: (root: Fellowship) => root.members?.length || 0,
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
🎭 **Business Logic** – Apply formatting, ACL, or transformations
|
|
256
|
+
```typescript
|
|
257
|
+
formattedName: {
|
|
258
|
+
type: () => String,
|
|
259
|
+
resolve: (root: Fellowship) => formatTitle(root.name),
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Using Custom Fields in Resolvers
|
|
264
|
+
|
|
265
|
+
To make custom fields available in your GraphQL resolvers, extend the generated `FieldsResolver` class:
|
|
266
|
+
|
|
267
|
+
```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
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
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.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Configuration
|
|
294
|
+
|
|
295
|
+
### Environment Variables
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
# Toggle between JSONB and string concatenation for JSON aggregation
|
|
299
|
+
D3GOP_USE_STRING_FOR_JSONB=true
|
|
300
|
+
|
|
301
|
+
# Control logging output
|
|
302
|
+
D3GOP_LOG_TYPE="debug" # Options: debug, disabled
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Common Commands
|
|
308
|
+
|
|
309
|
+
### Development
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Install dependencies
|
|
313
|
+
pnpm install
|
|
314
|
+
|
|
315
|
+
# Build the library
|
|
316
|
+
pnpm run build
|
|
317
|
+
|
|
318
|
+
# Watch mode for development
|
|
319
|
+
pnpm run watch
|
|
320
|
+
|
|
321
|
+
# Run all tests
|
|
322
|
+
pnpm test
|
|
323
|
+
|
|
324
|
+
# Run unit tests only
|
|
325
|
+
pnpm test:unit
|
|
326
|
+
|
|
327
|
+
# Run integration tests only
|
|
328
|
+
pnpm test:integration
|
|
329
|
+
|
|
330
|
+
# Watch mode for tests
|
|
331
|
+
pnpm test:watch
|
|
332
|
+
|
|
333
|
+
# Generate coverage report
|
|
334
|
+
pnpm test:coverage
|
|
335
|
+
|
|
336
|
+
# Build for publishing
|
|
337
|
+
pnpm run prepublishOnly
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Why GQL of Power?
|
|
343
|
+
|
|
344
|
+
### The Problem It Solves
|
|
345
|
+
|
|
346
|
+
Traditional GraphQL + ORM combinations suffer from the **N+1 query problem**:
|
|
347
|
+
|
|
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
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
GQL of Power: Fetch all data in **1 query** ✅
|
|
357
|
+
```
|
|
358
|
+
|
|
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.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Architecture Highlights
|
|
364
|
+
|
|
365
|
+
### Relationship Handling
|
|
366
|
+
|
|
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 |
|
|
373
|
+
|
|
374
|
+
### Performance Optimizations
|
|
375
|
+
|
|
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
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Known Limitations
|
|
384
|
+
|
|
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
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Contributing
|
|
403
|
+
|
|
404
|
+
Contributions are welcome! Whether you're fixing bugs, improving performance, or adding features, please submit a pull request.
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## License
|
|
409
|
+
|
|
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
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
> _"All we have to decide is what to do with the queries that are given us. And by using GQL of Power, that decision becomes much simpler."_
|
|
425
|
+
>
|
|
426
|
+
> — Adapted from J.R.R. Tolkien
|
package/bunfig.toml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[test]
|
|
2
|
+
coverage = true
|
|
3
|
+
# Only run tests from source directories, not dist
|
|
4
|
+
preload = ["./tests/setup.ts"]
|
|
5
|
+
testNamePattern = ".*\\.test\\.(ts|js)$"
|
|
6
|
+
|
|
7
|
+
# Explicitly exclude dist directory from test discovery
|
|
8
|
+
exclude = [
|
|
9
|
+
"dist/**/*",
|
|
10
|
+
"node_modules/**/*",
|
|
11
|
+
"coverage/**/*"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
# Include only tests from these directories
|
|
15
|
+
include = [
|
|
16
|
+
"tests/**/*.test.ts"
|
|
17
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getMetadataStorage } from 'type-graphql';
|
|
2
|
+
import { CustomFieldsSettings, FieldSettings, FieldsSettings, GQLEntityFilterInputFieldType, GQLEntityPaginationInputType, RelatedFieldSettings } from '../types';
|
|
3
|
+
import { AccessControlEntry } from '../types/access-control';
|
|
4
|
+
export declare const setGlobalConfig: (config: {
|
|
5
|
+
gqlTypesSuffix: string;
|
|
6
|
+
}) => void;
|
|
7
|
+
export declare const getFieldsOptionsFor: (name: string) => Record<string, string>;
|
|
8
|
+
export declare const getFieldByAlias: (entityName: string | undefined, alias: string) => string;
|
|
9
|
+
export declare const getCustomFieldsFor: (name: string) => CustomFieldsSettings<any>;
|
|
10
|
+
export declare const getACLFor: (name: string) => AccessControlEntry<any, any>;
|
|
11
|
+
export declare const getGQLEntityNameFor: (name: string) => string;
|
|
12
|
+
export declare const getGQLEntityNameForClass: <T>(classType: new () => T) => string;
|
|
13
|
+
export declare const getGQLEntityFieldResolverName: (gqlEntityName: string) => string;
|
|
14
|
+
export declare const getGQLEntityFieldResolverNameFor: <T extends Object>(classType: new () => T) => string;
|
|
15
|
+
export declare const getGQLEntityTypeFor: <T extends Object, K>(classType: new () => T) => string;
|
|
16
|
+
export declare function createGQLTypes<T extends Object, K>(classType: new () => T, opts: Partial<FieldsSettings<T>>, { customFields, acl, }?: {
|
|
17
|
+
customFields?: CustomFieldsSettings<T>;
|
|
18
|
+
acl?: AccessControlEntry<T, K>;
|
|
19
|
+
}): {
|
|
20
|
+
GQLEntity: {
|
|
21
|
+
new (): {
|
|
22
|
+
_____name: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
GQLEntityFilterInput: GQLEntityFilterInputFieldType<T>;
|
|
26
|
+
GQLEntityPaginationInputField: GQLEntityPaginationInputType<T>;
|
|
27
|
+
/**
|
|
28
|
+
* this can be used alongside `getGQLEntityTypeFor` to get the type of the entity, not sure it will be ever needed
|
|
29
|
+
*/
|
|
30
|
+
gqlEntityName: string;
|
|
31
|
+
relatedEntityName: string;
|
|
32
|
+
FieldsResolver: {
|
|
33
|
+
new (): {};
|
|
34
|
+
};
|
|
35
|
+
bindFieldResolvers: (c: any) => void;
|
|
36
|
+
};
|
|
37
|
+
type TypeGQLMetadataStorage = ReturnType<typeof getMetadataStorage>;
|
|
38
|
+
export declare function createGQLEntityFields<T, K>(fieldOptions: FieldSettings | RelatedFieldSettings<T>, fieldName: string, GQLEntity: new () => T, metadata: TypeGQLMetadataStorage, GQLEntityOrderBy: any, gqlEntityName: string, GQLEntityFilterInput: new () => K): void;
|
|
39
|
+
export {};
|