@dav3/gql-of-power 1.4.0 → 1.6.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 +192 -14
- package/dist/entities/gql-entity.d.ts +2 -0
- package/dist/entities/gql-entity.js +46 -46
- package/dist/field-operations.d.ts +47 -2
- package/dist/field-operations.js +38 -2
- package/dist/queries/enum-filter-converter.d.ts +29 -0
- package/dist/queries/enum-filter-converter.js +155 -0
- package/dist/queries/filter-processor.d.ts +21 -2
- package/dist/queries/filter-processor.js +56 -4
- package/dist/queries/gql-to-sql-mapper.js +50 -4
- package/dist/query-manager.d.ts +2 -5
- package/dist/query-manager.js +5 -105
- package/package.json +1 -1
- package/src/entities/gql-entity.ts +51 -53
- package/src/field-operations.ts +48 -3
- package/src/queries/enum-filter-converter.ts +179 -0
- package/src/queries/filter-processor.ts +69 -3
- package/src/queries/gql-to-sql-mapper.ts +67 -4
- package/src/query-manager.ts +13 -121
package/README.md
CHANGED
|
@@ -293,6 +293,68 @@ Returns `null` when the FK column is null (LEFT JOIN).
|
|
|
293
293
|
|
|
294
294
|
---
|
|
295
295
|
|
|
296
|
+
## Three Ways to Define an Entity
|
|
297
|
+
|
|
298
|
+
There are three APIs for registering a GQL entity. They differ in **when** the generated types (FilterInput, PaginationInput, FieldsResolver, etc.) are registered — that difference matters for filter-type resolution (see [Troubleshooting](#troubleshooting)).
|
|
299
|
+
|
|
300
|
+
### Quick comparison
|
|
301
|
+
|
|
302
|
+
| API | When to use | Calls `buildResolvers()`? | FilterInput registered |
|
|
303
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------ |
|
|
304
|
+
| `@GQLEntityClass` | **Default.** No circular imports between entity files. | ✅ Automatically (at decoration time) | ✅ Yes |
|
|
305
|
+
| `createGQLTypes()` | Same as `@GQLEntityClass` but as a function call (can't decorate a class, or prefer the explicit style). | ✅ Automatically (internally) | ✅ Yes |
|
|
306
|
+
| `createGQLEntity()` | **Circular imports** between entity definitions — you need to defer resolver registration. | ❌ **Deferred** — you must call `.buildResolvers()` yourself | ❌ No, until you call it |
|
|
307
|
+
|
|
308
|
+
### `@GQLEntityClass` — the default (recommended)
|
|
309
|
+
|
|
310
|
+
The decorator registers the entity **and** its FilterInput/FieldsResolver immediately at decoration time. All generated statics (`FilterInput`, `PaginationInput`, `FieldsResolver`) are available on the class.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
@GQLEntityClass(Book, bookFields)
|
|
314
|
+
export class BookGQL extends GQLEntityBase {}
|
|
315
|
+
// BookGQL.FilterInput is ready. Done.
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### `createGQLTypes()` — function-call equivalent
|
|
319
|
+
|
|
320
|
+
Same behaviour as the decorator — it calls `createGQLEntity().buildResolvers()` internally (see `gql-entity.ts:1221-1222`). Use it when you can't or don't want to decorate a class.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
export const BookGQL = createGQLTypes(Book, bookFields);
|
|
324
|
+
// BookGQL.FilterInput is ready. Done.
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### `createGQLEntity()` — for circular imports (advanced)
|
|
328
|
+
|
|
329
|
+
Returns the entity definition **without** registering the FilterInput or FieldsResolver. You must call `.buildResolvers()` yourself before `buildSchema()` runs. This split exists so two entity files can reference each other's `GQLEntity` type thunks without a circular-import deadlock at module-load time.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// book.entity.ts
|
|
333
|
+
export const BookEntity = createGQLEntity(Book, bookFields);
|
|
334
|
+
// ↑ FilterInput NOT registered yet
|
|
335
|
+
|
|
336
|
+
// author.entity.ts — can import BookEntity for the thunk even if Book imports Author
|
|
337
|
+
export const AuthorEntity = createGQLEntity(Author, authorFields, {
|
|
338
|
+
customFields: {/* ... uses () => BookEntity.GQLEntity ... */},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// schema/index.ts — YOU must call buildResolvers() on EVERY entity
|
|
342
|
+
export const BookGQL = BookEntity.buildResolvers(); // ← registers BookFilterInput
|
|
343
|
+
export const AuthorGQL = AuthorEntity.buildResolvers(); // ← registers AuthorFilterInput
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
> ⚠️ **If you forget a `.buildResolvers()` call**, any filter that references that entity (relationship fields, mapping custom-field filters, `_exists` filters) will throw at schema-build time:
|
|
347
|
+
>
|
|
348
|
+
> ```
|
|
349
|
+
> gql-of-power: FilterInput for referenced entity "Book" (BookFilterInput) is not registered.
|
|
350
|
+
> Make sure Book's buildResolvers() is called (or use createGQLTypes / @GQLEntityClass)
|
|
351
|
+
> before building the schema.
|
|
352
|
+
> ```
|
|
353
|
+
>
|
|
354
|
+
> The error names the missing entity so you know exactly which file to fix.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
296
358
|
## Filtering
|
|
297
359
|
|
|
298
360
|
Filter at any nesting level:
|
|
@@ -318,19 +380,32 @@ filter: {
|
|
|
318
380
|
|
|
319
381
|
### Filter operations
|
|
320
382
|
|
|
321
|
-
| Operation | Meaning
|
|
322
|
-
| -------------- |
|
|
323
|
-
| `_eq` | Equal
|
|
324
|
-
| `_ne` | Not equal
|
|
325
|
-
| `_in` | In array
|
|
326
|
-
| `_nin` | Not in array
|
|
327
|
-
| `_like` |
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
383
|
+
| Operation | Meaning |
|
|
384
|
+
| -------------- | ------------------------------------------------------- |
|
|
385
|
+
| `_eq` | Equal |
|
|
386
|
+
| `_ne` | Not equal |
|
|
387
|
+
| `_in` | In array |
|
|
388
|
+
| `_nin` | Not in array |
|
|
389
|
+
| `_like` | LIKE (case-sensitive contains) |
|
|
390
|
+
| `_nlike` | NOT LIKE |
|
|
391
|
+
| `_ilike` | ILIKE (case-insensitive contains) |
|
|
392
|
+
| `_nilike` | NOT ILIKE |
|
|
393
|
+
| `_startsWith` | LIKE prefix (case-sensitive) |
|
|
394
|
+
| `_istartsWith` | ILIKE prefix (case-insensitive) |
|
|
395
|
+
| `_endsWith` | LIKE suffix (case-sensitive) |
|
|
396
|
+
| `_iendsWith` | ILIKE suffix (case-insensitive) |
|
|
397
|
+
| `_re` | Regex match (`~`) |
|
|
398
|
+
| `_nre` | Regex non-match (`!~`) |
|
|
399
|
+
| `_gt` / `_gte` | Greater than / greater than or equal |
|
|
400
|
+
| `_lt` / `_lte` | Less than / less than or equal |
|
|
401
|
+
| `_between` | BETWEEN low AND high |
|
|
402
|
+
| `_nbetween` | NOT BETWEEN low AND high |
|
|
403
|
+
| `_is_null` | IS NULL (true) / IS NOT NULL (false) |
|
|
404
|
+
| `_and` | Logical AND |
|
|
405
|
+
| `_or` | Logical OR (generates UNION ALL) |
|
|
406
|
+
| `_not` | Negates a conjunction of conditions (NOT (...) wrapper) |
|
|
407
|
+
| `_exists` | Check related entities exist (AND-combined per key) |
|
|
408
|
+
| `_not_exists` | Check no related entities exist (AND-combined per key) |
|
|
334
409
|
|
|
335
410
|
### Existence Filters (`_exists` / `_not_exists`)
|
|
336
411
|
|
|
@@ -363,6 +438,28 @@ filter: {
|
|
|
363
438
|
|
|
364
439
|
`_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
440
|
|
|
441
|
+
### Negation Filter (`_not`)
|
|
442
|
+
|
|
443
|
+
Negate a set of conditions. `_not` takes an array of filter objects, AND-combines them, and wraps the result in `NOT (...)`.
|
|
444
|
+
|
|
445
|
+
```graphql
|
|
446
|
+
# Persons whose name is NOT "Sauron" AND race is NOT "Maiar"
|
|
447
|
+
filter: {
|
|
448
|
+
_not: [
|
|
449
|
+
{ name: "Sauron" }
|
|
450
|
+
{ race: "Maiar" }
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# Combine _not with other filters
|
|
455
|
+
filter: {
|
|
456
|
+
age_gt: 50
|
|
457
|
+
_not: [{ race: "Orc" }]
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Generates `NOT (cond1 AND cond2)` in the WHERE clause. For simple field-level negation, the negated operators (`_ne`, `_nin`) are more direct — `_not` is for negating compound expressions. Note: nesting `_or` inside `_not` is not fully negated (the UNION ALL path is not invertible); use De Morgan's law (`_and` of negated conditions) instead.
|
|
462
|
+
|
|
366
463
|
---
|
|
367
464
|
|
|
368
465
|
## Count Fields
|
|
@@ -560,9 +657,90 @@ No `limit` or `offset` parameters are accepted — `LIMIT 1` is always applied.
|
|
|
560
657
|
|
|
561
658
|
---
|
|
562
659
|
|
|
660
|
+
## Troubleshooting
|
|
661
|
+
|
|
662
|
+
### `FilterInput for referenced entity "X" is not registered`
|
|
663
|
+
|
|
664
|
+
This error fires at `buildSchema()` time when a relationship field, mapping custom-field filter, or `_exists` filter references an entity whose FilterInput was never registered.
|
|
665
|
+
|
|
666
|
+
**Cause:** You used `createGQLEntity()` (the deferred API) for entity `X` but forgot to call `X.buildResolvers()` before building the schema. The entity's ObjectType is registered, but its FilterInput is not.
|
|
667
|
+
|
|
668
|
+
**Fix:** Either switch the entity to `@GQLEntityClass` / `createGQLTypes()` (which call `buildResolvers()` automatically), or add the missing call:
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
// schema/index.ts — collect ALL entities and call buildResolvers()
|
|
672
|
+
const BookGQL = BookEntity.buildResolvers();
|
|
673
|
+
const AuthorGQL = AuthorEntity.buildResolvers();
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Before August 2026 this was a **silent wrong-type bug**: the missing FilterInput fell back to the parent entity's FilterInput, so `Author.books` would generate `AuthorFilterInput` instead of `BookFilterInput`. The library now throws with the message above.
|
|
677
|
+
|
|
678
|
+
### Mapping custom-field filter is missing from the schema
|
|
679
|
+
|
|
680
|
+
If a `customFields` entry with `mapping` doesn't produce its nested filter (e.g. `HomeRegion: { name_eq: '...' }` is absent), check whether `resolve` was **also** set on the same field:
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
customFields: {
|
|
684
|
+
homeRegion: {
|
|
685
|
+
type: () => RegionGQL.GQLEntity,
|
|
686
|
+
mapping: { refEntity: Region, refFields: 'id', fields: 'homeRegionId' },
|
|
687
|
+
generateFilter: true,
|
|
688
|
+
resolve: (root) => ..., // ← THIS makes the filter silently disappear
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
`resolve` and `mapping` are **mutually exclusive** on the same custom field. When both are set, only the resolver registers — the mapping filter is skipped. TypeScript's `CustomFieldSettings<T>` union enforces this at the type level, but `as any` bypasses it. Remove `resolve` (and `resolveDecorators`/`requires`) from a mapping-strategy field.
|
|
694
|
+
|
|
695
|
+
### Filter key is PascalCase, not camelCase
|
|
696
|
+
|
|
697
|
+
Mapping custom-field filters appear in the FilterInput under a **capitalized** key:
|
|
698
|
+
|
|
699
|
+
```graphql
|
|
700
|
+
# ✅ Correct — PascalCase filter key
|
|
701
|
+
persons(filter: { HomeRegion: { name_eq: 'Gondor' } }) { ... }
|
|
702
|
+
|
|
703
|
+
# ❌ Wrong — camelCase doesn't exist on the FilterInput
|
|
704
|
+
persons(filter: { homeRegion: { name_eq: 'Gondor' } })
|
|
705
|
+
# Field "homeRegion" is not defined by type "PersonFilterInput". Did you mean "HomeRegion"?
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
The field **selection** in the query stays camelCase (`homeRegion { ... }`); only the filter key is capitalized. Count-field nested filters follow the same rule (`BookCount: { _gte: 2 }`).
|
|
709
|
+
|
|
710
|
+
### Relationship-field filters aren't statically typed
|
|
711
|
+
|
|
712
|
+
`GQLEntityFilterInputFieldType<T>` surfaces filter operators for scalar fields but does **not** statically include nested relationship sub-filters (they're generated at runtime). Cast the filter object `as any` when using relationship filters:
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
queryManager.getQueryResultsForInfo(provider, PersonGQL, info, {
|
|
716
|
+
fellowship: { id_in: [1, 2] }, // works at runtime, not statically typed
|
|
717
|
+
} as any);
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
This is the library's own test-suite pattern.
|
|
721
|
+
|
|
722
|
+
### `mapNumericEnum` filter values silently fail inside nested/inline filters
|
|
723
|
+
|
|
724
|
+
If a `mapNumericEnum` field filters correctly at the **top level** but silently returns no results (or the wrong rows) when placed inside an inline field-argument filter on a nested relation — e.g.:
|
|
725
|
+
|
|
726
|
+
```graphql
|
|
727
|
+
# Works: top-level filter → 'InProgress' converted to 1
|
|
728
|
+
bearers(filter: { questState: InProgress }) { name }
|
|
729
|
+
|
|
730
|
+
# Silently failed before August 2026: inline filter → 'InProgress' passed as a string
|
|
731
|
+
rings { bearer(filter: { questState: InProgress }) { name } }
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**Cause:** enum conversion (`convertFilterEnumValues`) was historically applied only to top-level filters in `GQLQueryManager`. Inline field-argument filters (`books(filter: {...})`), count-field filters (`bookCount(filter: {...})`), and multi-level nested filters entered the SQL mapper through a different code path that bypassed conversion entirely. The `mapNumericEnum` string key went straight to SQL as a string instead of being converted to the numeric DB value, so the `WHERE` clause silently matched nothing.
|
|
735
|
+
|
|
736
|
+
**Fix (August 2026):** `handleFieldArguments` and `mapCountField` in the SQL mapper now call `convertFilterEnumValues` on inline filter args before SQL generation. The conversion resolves the target entity's `mapNumericEnum` fields through the parent entity's relation-field registry and recurses to arbitrary depth. This means filters now behave identically at the top level, inside `relation(filter:)`, inside nested `relation(filter: { nested: {...} })`, inside count subqueries, and inside `_or`/`_and` arrays at any level.
|
|
737
|
+
|
|
738
|
+
**If you still see this pattern:** ensure the target entity is decorator-registered (`@GQLEntityClass` / `createGQLTypes`) so its `mapNumericEnum` fields appear in the `MapEnumFieldsMap` registry — conversion silently passes values through for unregistered entities.
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
563
742
|
## Known Limitations
|
|
564
743
|
|
|
565
|
-
- ⚠️ Class-level `_not` conditions not yet supported
|
|
566
744
|
- ⚠️ Order by columns on related/joined tables not supported
|
|
567
745
|
- ⚠️ ACL pending async refactoring
|
|
568
746
|
|
|
@@ -29,6 +29,8 @@ export declare const clearMapEnumFields: () => void;
|
|
|
29
29
|
export declare const getParseJsonFieldsFor: (name: string) => Set<string>;
|
|
30
30
|
export declare const registerParseJsonField: (gqlEntityName: string, fieldName: string) => void;
|
|
31
31
|
export declare const clearParseJsonFields: () => void;
|
|
32
|
+
export declare const getRelationFieldsFor: (name: string) => Record<string, () => string>;
|
|
33
|
+
export declare const clearRelationFields: () => void;
|
|
32
34
|
export declare const getACLFor: (name: string) => AccessControlEntry<any, any>;
|
|
33
35
|
export declare const getGQLEntityNameFor: (name: string) => string;
|
|
34
36
|
export declare const getGQLEntityNameForClass: <T>(classType: new () => T) => string;
|
|
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.createGQLEntityFields = exports.GQLEntityBase = exports.getGQLEntityTypeFor = exports.getGQLEntityFieldResolverNameFor = exports.getGQLEntityFieldResolverName = exports.getGQLEntityNameForClass = exports.getGQLEntityNameFor = exports.getACLFor = exports.clearParseJsonFields = exports.registerParseJsonField = exports.getParseJsonFieldsFor = exports.clearMapEnumFields = exports.getMapEnumFieldsFor = exports.clearCountFields = exports.registerCountField = exports.getCountFieldsFor = exports.getCustomFieldsFor = exports.getFieldByAlias = exports.getFieldsOptionsFor = exports.setGlobalConfig = void 0;
|
|
9
|
+
exports.createGQLEntityFields = exports.GQLEntityBase = exports.getGQLEntityTypeFor = exports.getGQLEntityFieldResolverNameFor = exports.getGQLEntityFieldResolverName = exports.getGQLEntityNameForClass = exports.getGQLEntityNameFor = exports.getACLFor = exports.clearRelationFields = exports.getRelationFieldsFor = exports.clearParseJsonFields = exports.registerParseJsonField = exports.getParseJsonFieldsFor = exports.clearMapEnumFields = exports.getMapEnumFieldsFor = exports.clearCountFields = exports.registerCountField = exports.getCountFieldsFor = exports.getCustomFieldsFor = exports.getFieldByAlias = exports.getFieldsOptionsFor = exports.setGlobalConfig = void 0;
|
|
10
10
|
exports.getAutoResolvers = getAutoResolvers;
|
|
11
11
|
exports.defineFields = defineFields;
|
|
12
12
|
exports.GQLEntityClass = GQLEntityClass;
|
|
@@ -24,6 +24,14 @@ const CustomFieldsMap = {};
|
|
|
24
24
|
const CountFieldsMap = {};
|
|
25
25
|
const MapEnumFieldsMap = {};
|
|
26
26
|
const ParseJsonFieldsMap = {};
|
|
27
|
+
/**
|
|
28
|
+
* Relation fields declared via the plain `defineFields` pattern (not via
|
|
29
|
+
* `customFields`/`mapping`). Keyed by gqlEntityName → field name → target
|
|
30
|
+
* entity's ORM name (the raw string returned by `relatedEntityName()`). Used by
|
|
31
|
+
* `convertFilterEnumValues` to recurse into nested relation filters and apply
|
|
32
|
+
* the target entity's mapNumericEnum conversions.
|
|
33
|
+
*/
|
|
34
|
+
const RelationFieldsMap = {};
|
|
27
35
|
const aclMap = {};
|
|
28
36
|
/**
|
|
29
37
|
* Shared field-metadata registration used by both the @GQLEntityClass decorator
|
|
@@ -167,6 +175,14 @@ const clearParseJsonFields = () => {
|
|
|
167
175
|
}
|
|
168
176
|
};
|
|
169
177
|
exports.clearParseJsonFields = clearParseJsonFields;
|
|
178
|
+
const getRelationFieldsFor = (name) => RelationFieldsMap[name] ?? {};
|
|
179
|
+
exports.getRelationFieldsFor = getRelationFieldsFor;
|
|
180
|
+
const clearRelationFields = () => {
|
|
181
|
+
for (const key of Object.keys(RelationFieldsMap)) {
|
|
182
|
+
delete RelationFieldsMap[key];
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
exports.clearRelationFields = clearRelationFields;
|
|
170
186
|
const getACLFor = (name) => aclMap[name] ?? {};
|
|
171
187
|
exports.getACLFor = getACLFor;
|
|
172
188
|
const getGQLEntityNameFor = (name) => `${name}${gqlTypesSuffix || process.env['D3GOP_TYPES_SUFFIX'] || ''}`;
|
|
@@ -436,45 +452,6 @@ function _buildInputType(gqlEntityName, fields, opts, metadata, customFields) {
|
|
|
436
452
|
return GQLEntityInput;
|
|
437
453
|
}
|
|
438
454
|
// ─── Shared resolver builder ─────────────────────────────────────────────────
|
|
439
|
-
/** Phase 2: attach @FieldResolver methods for fields with `mapNumericEnum` (reverse-maps DB number → enum string). */
|
|
440
|
-
function _registerMapNumericEnumResolvers(rawFields, FieldsResolver) {
|
|
441
|
-
for (const fieldName of Object.keys(rawFields)) {
|
|
442
|
-
const fieldOpts = rawFields[fieldName];
|
|
443
|
-
if (!fieldOpts?.mapNumericEnum)
|
|
444
|
-
continue;
|
|
445
|
-
const fieldNameToUse = fieldOpts.alias ?? fieldName;
|
|
446
|
-
const enumTypeThunk = fieldOpts.type;
|
|
447
|
-
const resolveFn = (root) => {
|
|
448
|
-
const value = root[fieldNameToUse];
|
|
449
|
-
if (value === null || value === undefined)
|
|
450
|
-
return null;
|
|
451
|
-
try {
|
|
452
|
-
const enumObj = enumTypeThunk();
|
|
453
|
-
const key = enumObj[value];
|
|
454
|
-
if (typeof key === 'string')
|
|
455
|
-
return key;
|
|
456
|
-
for (const enumKey of Object.keys(enumObj)) {
|
|
457
|
-
if (enumObj[enumKey] === value)
|
|
458
|
-
return enumKey;
|
|
459
|
-
}
|
|
460
|
-
return value;
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
463
|
-
return value;
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
Object.defineProperty(FieldsResolver.prototype, fieldNameToUse, {
|
|
467
|
-
value: resolveFn,
|
|
468
|
-
writable: true,
|
|
469
|
-
configurable: true,
|
|
470
|
-
});
|
|
471
|
-
(0, type_graphql_1.FieldResolver)(enumTypeThunk, {
|
|
472
|
-
...fieldOpts.options,
|
|
473
|
-
name: fieldNameToUse,
|
|
474
|
-
})(FieldsResolver.prototype, fieldNameToUse, Object.getOwnPropertyDescriptor(FieldsResolver.prototype, fieldNameToUse));
|
|
475
|
-
(0, type_graphql_1.Root)()(FieldsResolver.prototype, fieldNameToUse, 0);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
455
|
/** Phase 3: process custom fields — alias registration, metadata, resolve/mapping/requiresRelations strategies. */
|
|
479
456
|
function _processCustomFields(customFields, gqlEntityName, GQLEntity, GQLEntityFilterInput, FieldsResolver, opts, metadata) {
|
|
480
457
|
CustomFieldsMap[gqlEntityName] = customFields;
|
|
@@ -597,7 +574,15 @@ function _registerMappingFieldFilter(fieldNameToUse, fieldOptions, GQLEntityFilt
|
|
|
597
574
|
target: GQLEntityFilterInput,
|
|
598
575
|
name: UppercasedFieldName,
|
|
599
576
|
schemaName: UppercasedFieldName,
|
|
600
|
-
getType: () =>
|
|
577
|
+
getType: () => {
|
|
578
|
+
const type = TypeMap[refFilterTypeName];
|
|
579
|
+
if (!type) {
|
|
580
|
+
throw new Error(`gql-of-power: FilterInput for referenced entity "${refGqlEntityName}" (${refFilterTypeName}) ` +
|
|
581
|
+
`is not registered. Make sure ${refGqlEntityName}'s buildResolvers() is called ` +
|
|
582
|
+
`(or use createGQLTypes / @GQLEntityClass) before building the schema.`);
|
|
583
|
+
}
|
|
584
|
+
return type;
|
|
585
|
+
},
|
|
601
586
|
typeOptions: { nullable: true },
|
|
602
587
|
complexity: undefined,
|
|
603
588
|
description: `Filter by ${fieldNameToUse} fields`,
|
|
@@ -788,7 +773,15 @@ function _registerExistsFilters(gqlEntityName, fields, opts, metadata, GQLEntity
|
|
|
788
773
|
target: GQLEntityExistsFilterInput,
|
|
789
774
|
name: relFieldName,
|
|
790
775
|
schemaName: relFieldName,
|
|
791
|
-
getType: () =>
|
|
776
|
+
getType: () => {
|
|
777
|
+
const type = TypeMap[relFilterTypeName];
|
|
778
|
+
if (!type) {
|
|
779
|
+
throw new Error(`gql-of-power: FilterInput for related entity "${relGQLEntityName}" (${relFilterTypeName}) ` +
|
|
780
|
+
`is not registered. Make sure ${relGQLEntityName}'s buildResolvers() is called ` +
|
|
781
|
+
`(or use createGQLTypes / @GQLEntityClass) before building the schema.`);
|
|
782
|
+
}
|
|
783
|
+
return type;
|
|
784
|
+
},
|
|
792
785
|
typeOptions: { nullable: true },
|
|
793
786
|
complexity: undefined,
|
|
794
787
|
description: `Filter ${relFieldName} by their fields for existence check`,
|
|
@@ -850,10 +843,7 @@ function _buildResolversForEntity(GQLEntity, gqlEntityName, fields, opts, metada
|
|
|
850
843
|
FieldsResolver = __decorate([
|
|
851
844
|
(0, type_graphql_1.Resolver)(() => GQLEntity)
|
|
852
845
|
], FieldsResolver);
|
|
853
|
-
// ─── Phases
|
|
854
|
-
if (rawFields) {
|
|
855
|
-
_registerMapNumericEnumResolvers(rawFields, FieldsResolver);
|
|
856
|
-
}
|
|
846
|
+
// ─── Phases 3-6: delegate to focused helpers ────────────────────────────
|
|
857
847
|
if (customFields) {
|
|
858
848
|
_processCustomFields(customFields, gqlEntityName, GQLEntity, GQLEntityFilterInput, FieldsResolver, opts, metadata);
|
|
859
849
|
}
|
|
@@ -1003,6 +993,16 @@ function createGQLEntityFilters(fieldOptions, fieldName, GQLEntity, metadata, GQ
|
|
|
1003
993
|
deprecationReason: undefined,
|
|
1004
994
|
};
|
|
1005
995
|
metadata.collectClassFieldMetadata(fieldFilter);
|
|
996
|
+
// Register relation fields for nested-filter enum conversion. The nested
|
|
997
|
+
// FilterInput is exposed under both the camelCased fieldName (above) and the
|
|
998
|
+
// UppercasedFieldName — record both so convertFilterEnumValues can resolve
|
|
999
|
+
// whichever key the caller uses.
|
|
1000
|
+
if ('relatedEntityName' in fieldOptions && fieldOptions.relatedEntityName) {
|
|
1001
|
+
RelationFieldsMap[gqlEntityName] = RelationFieldsMap[gqlEntityName] || {};
|
|
1002
|
+
const relThunk = fieldOptions.relatedEntityName;
|
|
1003
|
+
RelationFieldsMap[gqlEntityName][fieldName] = relThunk;
|
|
1004
|
+
RelationFieldsMap[gqlEntityName][UppercasedFieldName] = relThunk;
|
|
1005
|
+
}
|
|
1006
1006
|
if ('array' in fieldOptions && 'relatedEntityName' in fieldOptions) {
|
|
1007
1007
|
const relatedEntityName = (0, exports.getGQLEntityNameFor)(fieldOptions.relatedEntityName());
|
|
1008
1008
|
metadata.collectHandlerParamMetadata({
|
|
@@ -19,13 +19,22 @@ export declare abstract class FieldOperationsClass<T> {
|
|
|
19
19
|
abstract _lt: T;
|
|
20
20
|
abstract _lte: T;
|
|
21
21
|
abstract _like: T;
|
|
22
|
-
abstract
|
|
22
|
+
abstract _nlike: T;
|
|
23
23
|
abstract _ilike: T;
|
|
24
|
+
abstract _nilike: T;
|
|
25
|
+
abstract _startsWith: T;
|
|
26
|
+
abstract _istartsWith: T;
|
|
27
|
+
abstract _endsWith: T;
|
|
28
|
+
abstract _iendsWith: T;
|
|
29
|
+
abstract _re: T;
|
|
30
|
+
abstract _nre: T;
|
|
24
31
|
abstract _fulltext: T;
|
|
25
32
|
abstract _overlap: T[];
|
|
26
33
|
abstract _contains: T;
|
|
27
34
|
abstract _contained: T;
|
|
28
35
|
abstract _between: T[];
|
|
36
|
+
abstract _nbetween: T[];
|
|
37
|
+
abstract _is_null: T;
|
|
29
38
|
abstract _exists: T;
|
|
30
39
|
}
|
|
31
40
|
type Scalar = string | number | boolean | bigint | null;
|
|
@@ -69,7 +78,7 @@ export declare const FieldOperations: {
|
|
|
69
78
|
where: string;
|
|
70
79
|
value: undefined;
|
|
71
80
|
};
|
|
72
|
-
|
|
81
|
+
_nlike: ([l, r]: string[], []: Scalar[]) => {
|
|
73
82
|
where: string;
|
|
74
83
|
value: undefined;
|
|
75
84
|
};
|
|
@@ -77,6 +86,34 @@ export declare const FieldOperations: {
|
|
|
77
86
|
where: string;
|
|
78
87
|
value: undefined;
|
|
79
88
|
};
|
|
89
|
+
_nilike: ([l, r]: string[], []: Scalar[]) => {
|
|
90
|
+
where: string;
|
|
91
|
+
value: undefined;
|
|
92
|
+
};
|
|
93
|
+
_startsWith: ([l, r]: string[], []: Scalar[]) => {
|
|
94
|
+
where: string;
|
|
95
|
+
value: undefined;
|
|
96
|
+
};
|
|
97
|
+
_istartsWith: ([l, r]: string[], []: Scalar[]) => {
|
|
98
|
+
where: string;
|
|
99
|
+
value: undefined;
|
|
100
|
+
};
|
|
101
|
+
_endsWith: ([l, r]: string[], []: Scalar[]) => {
|
|
102
|
+
where: string;
|
|
103
|
+
value: undefined;
|
|
104
|
+
};
|
|
105
|
+
_iendsWith: ([l, r]: string[], []: Scalar[]) => {
|
|
106
|
+
where: string;
|
|
107
|
+
value: undefined;
|
|
108
|
+
};
|
|
109
|
+
_re: ([l, r]: string[], []: Scalar[]) => {
|
|
110
|
+
where: string;
|
|
111
|
+
value: undefined;
|
|
112
|
+
};
|
|
113
|
+
_nre: ([l, r]: string[], []: Scalar[]) => {
|
|
114
|
+
where: string;
|
|
115
|
+
value: undefined;
|
|
116
|
+
};
|
|
80
117
|
_fulltext: ([l, r]: string[], []: Scalar[]) => {
|
|
81
118
|
where: string;
|
|
82
119
|
value: undefined;
|
|
@@ -93,6 +130,14 @@ export declare const FieldOperations: {
|
|
|
93
130
|
where: string;
|
|
94
131
|
value: undefined;
|
|
95
132
|
};
|
|
133
|
+
_nbetween: ([l, r1, r2]: string[], []: Scalar[]) => {
|
|
134
|
+
where: string;
|
|
135
|
+
value: undefined;
|
|
136
|
+
};
|
|
137
|
+
_is_null: ([l]: string[], [_l, rv]: Scalar[]) => {
|
|
138
|
+
where: string;
|
|
139
|
+
value: undefined;
|
|
140
|
+
};
|
|
96
141
|
_exists: ([l]: string[], []: Scalar[]) => {
|
|
97
142
|
where: string;
|
|
98
143
|
value: undefined;
|
package/dist/field-operations.js
CHANGED
|
@@ -64,14 +64,42 @@ exports.FieldOperations = {
|
|
|
64
64
|
where: `${l} like ${r}`,
|
|
65
65
|
value: undefined,
|
|
66
66
|
}),
|
|
67
|
-
|
|
68
|
-
where: `${l}
|
|
67
|
+
_nlike: ([l, r], []) => ({
|
|
68
|
+
where: `${l} not like ${r}`,
|
|
69
69
|
value: undefined,
|
|
70
70
|
}),
|
|
71
71
|
_ilike: ([l, r], []) => ({
|
|
72
72
|
where: `${l} ilike ${r}`,
|
|
73
73
|
value: undefined,
|
|
74
74
|
}),
|
|
75
|
+
_nilike: ([l, r], []) => ({
|
|
76
|
+
where: `${l} not ilike ${r}`,
|
|
77
|
+
value: undefined,
|
|
78
|
+
}),
|
|
79
|
+
_startsWith: ([l, r], []) => ({
|
|
80
|
+
where: `${l} like ${r} || '%'`,
|
|
81
|
+
value: undefined,
|
|
82
|
+
}),
|
|
83
|
+
_istartsWith: ([l, r], []) => ({
|
|
84
|
+
where: `${l} ilike ${r} || '%'`,
|
|
85
|
+
value: undefined,
|
|
86
|
+
}),
|
|
87
|
+
_endsWith: ([l, r], []) => ({
|
|
88
|
+
where: `${l} like '%' || ${r}`,
|
|
89
|
+
value: undefined,
|
|
90
|
+
}),
|
|
91
|
+
_iendsWith: ([l, r], []) => ({
|
|
92
|
+
where: `${l} ilike '%' || ${r}`,
|
|
93
|
+
value: undefined,
|
|
94
|
+
}),
|
|
95
|
+
_re: ([l, r], []) => ({
|
|
96
|
+
where: `${l} ~ ${r}`,
|
|
97
|
+
value: undefined,
|
|
98
|
+
}),
|
|
99
|
+
_nre: ([l, r], []) => ({
|
|
100
|
+
where: `${l} !~ ${r}`,
|
|
101
|
+
value: undefined,
|
|
102
|
+
}),
|
|
75
103
|
_fulltext: ([l, r], []) => ({
|
|
76
104
|
where: `${l}::tsvector @@ ${r}::tsquery`,
|
|
77
105
|
value: undefined,
|
|
@@ -88,6 +116,14 @@ exports.FieldOperations = {
|
|
|
88
116
|
where: `${l} between ${r1} and ${r2}`,
|
|
89
117
|
value: undefined,
|
|
90
118
|
}),
|
|
119
|
+
_nbetween: ([l, r1, r2], []) => ({
|
|
120
|
+
where: `${l} not between ${r1} and ${r2}`,
|
|
121
|
+
value: undefined,
|
|
122
|
+
}),
|
|
123
|
+
_is_null: ([l], [_l, rv]) => ({
|
|
124
|
+
where: rv ? `${l} is null` : `${l} is not null`,
|
|
125
|
+
value: undefined,
|
|
126
|
+
}),
|
|
91
127
|
_exists: ([l], []) => ({
|
|
92
128
|
where: `exists ${l}`,
|
|
93
129
|
value: undefined,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare function toDbValue(value: any, enumObj: any, isArray: boolean): any;
|
|
2
|
+
export declare function findEnumFieldName(key: string, enumFields: Record<string, any>): string | null;
|
|
3
|
+
/** Convert an operator-keyed object (e.g. { _eq: 'X', _in: [...] }) whose target is an enum field. */
|
|
4
|
+
export declare function convertOperatorObject(operatorObject: Record<string, any>, enumObj: any): Record<string, any>;
|
|
5
|
+
/** Recurse into a mapped custom-field nested filter using the referenced entity's enums. */
|
|
6
|
+
export declare function convertMappedCustomField(key: string, value: any, mappedCustomFields: Record<string, {
|
|
7
|
+
mapping: {
|
|
8
|
+
refEntity: new () => any;
|
|
9
|
+
};
|
|
10
|
+
}>): any;
|
|
11
|
+
/**
|
|
12
|
+
* Recurse into a plain-`defineFields` relation nested filter using the target
|
|
13
|
+
* entity's enums. The relation field is looked up in `RelationFieldsMap`
|
|
14
|
+
* (registered by `createGQLEntityFilters`), which stores the target entity's
|
|
15
|
+
* ORM name thunk. Falls back to returning the value untouched if the key isn't
|
|
16
|
+
* a known relation field.
|
|
17
|
+
*/
|
|
18
|
+
export declare function convertRelationField(key: string, value: any, relationFields: Record<string, () => string>): any;
|
|
19
|
+
export declare function convertFilterEnumValues(filter: any, enumFields: Record<string, any>, mappedCustomFields?: Record<string, {
|
|
20
|
+
mapping: {
|
|
21
|
+
refEntity: new () => any;
|
|
22
|
+
};
|
|
23
|
+
}>, relationFields?: Record<string, () => string>): any;
|
|
24
|
+
/** Resolve a single filter key/value to its enum-converted form. */
|
|
25
|
+
export declare function convertFilterEntry(key: string, value: any, enumFields: Record<string, any>, mappedCustomFields?: Record<string, {
|
|
26
|
+
mapping: {
|
|
27
|
+
refEntity: new () => any;
|
|
28
|
+
};
|
|
29
|
+
}>, relationFields?: Record<string, () => string>): any;
|