@dav3/gql-of-power 1.2.0 → 1.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # ⚔️ GQL of Power
2
2
 
3
- > **"One Query to Rule Them All"** — Master your data with the power of a single GraphQL query
3
+ > **"One Query to Rule Them All"** — Master your data with the power of a single optimized SQL query
4
4
 
5
5
  ---
6
6
 
@@ -8,9 +8,9 @@
8
8
 
9
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
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?
11
+ But what if there was another way? What if, with the power of a single SQL 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 SQL 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
 
@@ -157,6 +157,7 @@ const fields = defineFields(Book, {
157
157
  | `alias` | `string` | Override the GQL field name |
158
158
  | `array` | `true` | Mark as array return type |
159
159
  | `relatedEntityName` | `() => string` | ORM entity name for array relation fields (auto-derived when using `@GQLEntityClass`) |
160
+ | `countFieldName` | `string` | Generate a count field for this relationship (see [Count Fields](#count-fields)) |
160
161
  | `enum` | `EnumData` | Register an enum type |
161
162
 
162
163
  ### `@GQLEntityClass(OrmClass, fields, extra?)`
@@ -317,17 +318,114 @@ filter: {
317
318
 
318
319
  ### Filter operations
319
320
 
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) |
321
+ | Operation | Meaning |
322
+ | -------------- | ------------------------------------------------------ |
323
+ | `_eq` | Equal |
324
+ | `_ne` | Not equal |
325
+ | `_in` | In array |
326
+ | `_nin` | Not in array |
327
+ | `_like` | ILIKE (case-insensitive contains) |
328
+ | `_gt` / `_gte` | Greater than / greater than or equal |
329
+ | `_lt` / `_lte` | Less than / less than or equal |
330
+ | `_and` | Logical AND |
331
+ | `_or` | Logical OR (generates UNION ALL) |
332
+ | `_exists` | Check related entities exist (AND-combined per key) |
333
+ | `_not_exists` | Check no related entities exist (AND-combined per key) |
334
+
335
+ ### Existence Filters (`_exists` / `_not_exists`)
336
+
337
+ Check whether related entities match a set of conditions. Each key is a relationship field name; the value is a filter applied to that related entity. Multiple keys are AND-combined.
338
+
339
+ ```graphql
340
+ # Authors that have at least one book titled "The Hobbit"
341
+ filter: {
342
+ _exists: {
343
+ books: { title: "The Hobbit" }
344
+ }
345
+ }
346
+
347
+ # Authors that have a book titled "The Hobbit" AND no books in the "Horror" genre
348
+ filter: {
349
+ _and: [
350
+ { _exists: { books: { title: "The Hobbit" } } }
351
+ { _not_exists: { books: { genre: "Horror" } } }
352
+ ]
353
+ }
354
+
355
+ # Persons who fought in a victory battle AND wrote a book about it
356
+ filter: {
357
+ _exists: {
358
+ battles: { outcome: "Victory" }
359
+ books: { title: { _like: "%War%" } }
360
+ }
361
+ }
362
+ ```
363
+
364
+ `_exists` generates `EXISTS (SELECT 1 FROM ... WHERE ...)` subqueries. `_not_exists` generates `NOT EXISTS (...)`. Multiple keys within one `_exists`/`_not_exists` each produce a separate `EXISTS`/`NOT EXISTS` clause, AND-combined in the WHERE. OR across exists conditions is achieved via `_or`.
365
+
366
+ ---
367
+
368
+ ## Count Fields
369
+
370
+ Add `countFieldName` to any relationship field with `array: true` to auto-generate an Int count field. The count is computed via a correlated `COUNT(*)` subquery — no JOINs in the outer query.
371
+
372
+ ### Definition
373
+
374
+ ```typescript
375
+ const authorFields = defineFields(Author, {
376
+ id: { type: () => ID, generateFilter: true },
377
+ name: { type: () => String, generateFilter: true },
378
+ books: {
379
+ type: () => BookGQL,
380
+ array: true,
381
+ relatedEntityName: () => 'Book',
382
+ countFieldName: 'bookCount', // generates an `bookCount: Int` field
383
+ },
384
+ });
385
+ ```
386
+
387
+ ### Querying
388
+
389
+ ```graphql
390
+ query {
391
+ authors {
392
+ name
393
+ bookCount # total books
394
+ bookCount(filter: { publishedYear: { _gt: 1950 } }) # filtered count
395
+ }
396
+ }
397
+ ```
398
+
399
+ ### Filtering by Count
400
+
401
+ The count field is also available as a filter key with numeric operators:
402
+
403
+ ```graphql
404
+ # Authors with exactly 4 books
405
+ filter: { bookCount: 4 }
406
+
407
+ # Authors with more than 3 books
408
+ filter: { bookCount_gt: 3 }
409
+
410
+ # Nested object form
411
+ filter: { BookCount: { _gte: 2, _lte: 10 } }
412
+ ```
413
+
414
+ Supported operators: `_eq`, `_ne`, `_gt`, `_gte`, `_lt`, `_lte`.
415
+
416
+ ### Generated SQL
417
+
418
+ ```sql
419
+ -- bookCount (select)
420
+ SELECT ...,
421
+ (SELECT COUNT(*) FROM "books" AS e_w1 WHERE e_w1.author_id = a_1.id) AS "bookCount"
422
+ FROM authors AS a_1 ...
423
+
424
+ -- bookCount_gt (filter)
425
+ WHERE (SELECT COUNT(*) FROM "books" AS e_w1 WHERE e_w1.author_id = a_1.id) > :v_bookCount_gt__1
426
+ ```
427
+
428
+ Works for all relationship types (1:m, m:1, m:n). For m:n, the pivot table is included in the subquery.
331
429
 
332
430
  ---
333
431
 
@@ -409,6 +507,34 @@ const results = await queryManager.getQueryResultsForFields(
409
507
 
410
508
  `BookGQL.relatedEntityName` (`'Book'`) is used automatically to look up ORM metadata — no need to pass the ORM class separately.
411
509
 
510
+ ### Singular Queries
511
+
512
+ For fetching a single record, use the singular variants. These enforce `LIMIT 1` internally and return `K | null` instead of `K[]`:
513
+
514
+ ```typescript
515
+ // From a GraphQL resolver
516
+ const ring = await queryManager.getQueryResultForInfo(
517
+ metadataProvider,
518
+ RingGQL,
519
+ info,
520
+ { forgedBy: 'Sauron' },
521
+ [{ forgedYear: 'desc' }] // optional orderBy — controls *which* record is returned
522
+ );
523
+ // ring: Ring | null
524
+
525
+ // With explicit field selection
526
+ const ring = await queryManager.getQueryResultForFields(
527
+ metadataProvider,
528
+ RingGQL,
529
+ { id: {}, name: {} },
530
+ { forgedBy: 'Sauron' },
531
+ [{ forgedYear: 'desc' }]
532
+ );
533
+ // ring: Ring | null
534
+ ```
535
+
536
+ No `limit` or `offset` parameters are accepted — `LIMIT 1` is always applied.
537
+
412
538
  ---
413
539
 
414
540
  ## Architecture
@@ -1,5 +1,5 @@
1
1
  import { getMetadataStorage } from 'type-graphql';
2
- import { CustomFieldsSettings, FieldSettings, FieldsSettings, GQLEntityFilterInputFieldType, GQLEntityPaginationInputType, RelatedFieldSettings } from '../types';
2
+ import { CountFieldMeta, CustomFieldsSettings, FieldSettings, FieldsSettings, GQLEntityFilterInputFieldType, GQLEntityPaginationInputType, RelatedFieldSettings } from '../types';
3
3
  import { AccessControlEntry } from '../types/access-control';
4
4
  export declare const setGlobalConfig: (config: {
5
5
  gqlTypesSuffix?: string;
@@ -8,6 +8,26 @@ export declare const setGlobalConfig: (config: {
8
8
  export declare const getFieldsOptionsFor: (name: string) => Record<string, string>;
9
9
  export declare const getFieldByAlias: (entityName: string | undefined, alias: string) => string;
10
10
  export declare const getCustomFieldsFor: (name: string) => CustomFieldsSettings<any>;
11
+ /**
12
+ * Returns the count fields registered for the given GQL entity name.
13
+ * Keyed by the count field name (e.g. 'bookCount'), value is the count field metadata.
14
+ */
15
+ export declare const getCountFieldsFor: (name: string) => Record<string, CountFieldMeta>;
16
+ /**
17
+ * Manually registers a count field for an entity.
18
+ * Useful for testing without the @GQLEntityClass decorator.
19
+ * In production, count fields are auto-registered by the decorator when `countFieldName` is set.
20
+ */
21
+ export declare const registerCountField: (gqlEntityName: string, countFieldName: string, relationshipFieldName: string, relatedEntityName: () => string) => void;
22
+ /**
23
+ * Clears all registered count fields. Intended for test teardown.
24
+ */
25
+ export declare const clearCountFields: () => void;
26
+ export declare const getMapEnumFieldsFor: (name: string) => Record<string, any>;
27
+ export declare const clearMapEnumFields: () => void;
28
+ export declare const getParseJsonFieldsFor: (name: string) => Set<string>;
29
+ export declare const registerParseJsonField: (gqlEntityName: string, fieldName: string) => void;
30
+ export declare const clearParseJsonFields: () => void;
11
31
  export declare const getACLFor: (name: string) => AccessControlEntry<any, any>;
12
32
  export declare const getGQLEntityNameFor: (name: string) => string;
13
33
  export declare const getGQLEntityNameForClass: <T>(classType: new () => T) => string;
@@ -34,6 +54,7 @@ export type GQLEntityStaticMembers = {
34
54
  readonly FilterInput: new () => any;
35
55
  readonly PaginationInput: new () => any;
36
56
  readonly OrderBy: new () => any;
57
+ readonly Input: new () => any;
37
58
  readonly FieldsResolver: new () => any;
38
59
  readonly gqlEntityName: string;
39
60
  readonly relatedEntityName: string;
@@ -54,6 +75,7 @@ export declare abstract class GQLEntityBase {
54
75
  static FilterInput: new () => any;
55
76
  static PaginationInput: new () => any;
56
77
  static OrderBy: new () => any;
78
+ static Input: new () => any;
57
79
  static FieldsResolver: new () => any;
58
80
  static gqlEntityName: string;
59
81
  static relatedEntityName: string;
@@ -140,12 +162,14 @@ export declare function createGQLEntity<T extends Object, K>(classType: new () =
140
162
  GQLEntityOrderBy: {
141
163
  new (): {};
142
164
  };
165
+ GQLEntityInput: new () => any;
143
166
  FieldsResolver: {
144
167
  new (): {};
145
168
  };
146
169
  bindFieldResolvers: (_c: any) => void;
147
170
  };
148
171
  };
172
+ type TypeGQLMetadataStorage = ReturnType<typeof getMetadataStorage>;
149
173
  /**
150
174
  * Convenience wrapper — creates entity, builds resolvers, and returns everything merged.
151
175
  * Equivalent to calling `createGQLEntity(...).buildResolvers()` and merging results.
@@ -160,6 +184,7 @@ export declare function createGQLTypes<T extends Object, K>(classType: new () =>
160
184
  GQLEntityOrderBy: {
161
185
  new (): {};
162
186
  };
187
+ GQLEntityInput: new () => any;
163
188
  FieldsResolver: {
164
189
  new (): {};
165
190
  };
@@ -177,13 +202,13 @@ export declare function createGQLTypes<T extends Object, K>(classType: new () =>
177
202
  GQLEntityOrderBy: {
178
203
  new (): {};
179
204
  };
205
+ GQLEntityInput: new () => any;
180
206
  FieldsResolver: {
181
207
  new (): {};
182
208
  };
183
209
  bindFieldResolvers: (_c: any) => void;
184
210
  };
185
211
  };
186
- type TypeGQLMetadataStorage = ReturnType<typeof getMetadataStorage>;
187
212
  /**
188
213
  * Creates filter and sorting metadata for a field. Called during buildResolvers().
189
214
  */