@dav3/gql-of-power 1.5.0 → 1.7.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 +126 -17
- package/dist/entities/gql-entity.d.ts +7 -0
- package/dist/entities/gql-entity.js +36 -39
- package/dist/field-operations.d.ts +47 -2
- package/dist/field-operations.js +38 -2
- package/dist/queries/filter-processor.d.ts +21 -2
- package/dist/queries/filter-processor.js +56 -4
- package/dist/queries/gql-to-sql-mapper.d.ts +2 -2
- package/dist/queries/gql-to-sql-mapper.js +56 -6
- package/dist/queries/sql-builder.d.ts +20 -0
- package/dist/queries/sql-builder.js +33 -0
- package/dist/types/sql-types.d.ts +27 -0
- package/package.json +15 -3
- package/src/entities/gql-entity.ts +43 -50
- package/src/field-operations.ts +48 -3
- package/src/queries/filter-processor.ts +69 -3
- package/src/queries/gql-to-sql-mapper.ts +73 -9
- package/src/queries/sql-builder.ts +33 -0
- package/src/types/sql-types.ts +27 -0
package/README.md
CHANGED
|
@@ -380,19 +380,32 @@ filter: {
|
|
|
380
380
|
|
|
381
381
|
### Filter operations
|
|
382
382
|
|
|
383
|
-
| Operation | Meaning
|
|
384
|
-
| -------------- |
|
|
385
|
-
| `_eq` | Equal
|
|
386
|
-
| `_ne` | Not equal
|
|
387
|
-
| `_in` | In array
|
|
388
|
-
| `_nin` | Not in array
|
|
389
|
-
| `_like` |
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
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) |
|
|
396
409
|
|
|
397
410
|
### Existence Filters (`_exists` / `_not_exists`)
|
|
398
411
|
|
|
@@ -425,6 +438,28 @@ filter: {
|
|
|
425
438
|
|
|
426
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`.
|
|
427
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
|
+
|
|
428
463
|
---
|
|
429
464
|
|
|
430
465
|
## Count Fields
|
|
@@ -526,6 +561,7 @@ pagination: {
|
|
|
526
561
|
| `D3GOP_LOG_TYPE` | Logging level: `debug` or `disabled` |
|
|
527
562
|
| `D3GOP_DEFAULT_QUERY_LIMIT` | Default query limit when pagination is not specified (default: `3000`) |
|
|
528
563
|
| `D3GOP_USE_STRING_FOR_JSONB` | Toggle between JSONB and string concatenation for JSON aggregation |
|
|
564
|
+
| `GQL_OF_POWER_MAP_ENUM_OUTPUT` | Enum output mode: `raw` (default) or `key`. See [mapEnumOutput](#mapenumoutput-sdl-schema-support) below. |
|
|
529
565
|
|
|
530
566
|
> **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.
|
|
531
567
|
|
|
@@ -539,6 +575,9 @@ setGlobalConfig({ gqlTypesSuffix: 'V2' });
|
|
|
539
575
|
|
|
540
576
|
// Optionally set a separate suffix for sort/pagination types
|
|
541
577
|
setGlobalConfig({ gqlTypesSuffix: 'V2', gqlSortSuffix: 'V2' });
|
|
578
|
+
|
|
579
|
+
// Enum output mode for SDL-rebuilt schemas (Apollo Server + .graphql file)
|
|
580
|
+
setGlobalConfig({ mapEnumOutput: 'key' });
|
|
542
581
|
```
|
|
543
582
|
|
|
544
583
|
---
|
|
@@ -704,9 +743,60 @@ rings { bearer(filter: { questState: InProgress }) { name } }
|
|
|
704
743
|
|
|
705
744
|
---
|
|
706
745
|
|
|
746
|
+
### `mapEnumOutput` — SDL schema support
|
|
747
|
+
|
|
748
|
+
#### The problem
|
|
749
|
+
|
|
750
|
+
If your app builds the GraphQL schema from a **pre-generated SDL file** (e.g. Apollo Server + `buildASTSchema(parse(schemaSDL))`) instead of calling type-graphql's `buildSchema()` live, `mapNumericEnum` fields will throw:
|
|
751
|
+
|
|
752
|
+
```
|
|
753
|
+
GraphQLError: Enum "ScheduleOverrideLineStateCode" cannot represent value: 0
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
#### Why
|
|
757
|
+
|
|
758
|
+
SDL has no syntax for numeric enum values. `enum StateCode { ACTIVE, INACTIVE }` is all SDL can express — there's no `= 0`. When graphql-js rebuilds the schema from SDL text, it defaults each enum value to its name string (`{ ACTIVE: { value: "ACTIVE" } }`) instead of the original number (`{ ACTIVE: { value: 0 } }`). So `serialize(0)` fails because `0` isn't in the lookup map — only `"ACTIVE"` is.
|
|
759
|
+
|
|
760
|
+
Native `buildSchema()` preserves the in-memory TypeScript enum objects, so this only affects the SDL-rebuilt path.
|
|
761
|
+
|
|
762
|
+
#### Fix
|
|
763
|
+
|
|
764
|
+
Enable `mapEnumOutput: 'key'`. This wraps enum columns in a SQL `CASE WHEN 0 THEN 'ACTIVE' END` expression so the query returns the string key directly — which the SDL-rebuilt schema can serialize.
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
# Env var (recommended for Lambda/Apollo setups)
|
|
768
|
+
GQL_OF_POWER_MAP_ENUM_OUTPUT=key
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
// Or programmatically
|
|
773
|
+
import { setGlobalConfig } from '@dav3/gql-of-power';
|
|
774
|
+
setGlobalConfig({ mapEnumOutput: 'key' });
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
// Or per-field
|
|
779
|
+
const fields = defineFields(MyEntity, {
|
|
780
|
+
stateCode: {
|
|
781
|
+
type: () => StateCode,
|
|
782
|
+
mapNumericEnum: true,
|
|
783
|
+
mapEnumOutput: 'key', // override per field
|
|
784
|
+
generateFilter: true,
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
| Mode | SQL output | Works with |
|
|
790
|
+
|------|-----------|------------|
|
|
791
|
+
| `'raw'` (default) | `e_a1.state_code AS "stateCode"` | Live `buildSchema()` — graphql-js has `{ value: 0 }` |
|
|
792
|
+
| `'key'` | `CASE e_a1.state_code WHEN 0 THEN 'ACTIVE' ... END AS "stateCode"` | SDL-rebuilt schema — graphql-js has `{ value: "ACTIVE" }` |
|
|
793
|
+
|
|
794
|
+
Both modes produce identical JSON output: enum fields come back as string keys (`"ACTIVE"`).
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
707
798
|
## Known Limitations
|
|
708
799
|
|
|
709
|
-
- ⚠️ Class-level `_not` conditions not yet supported
|
|
710
800
|
- ⚠️ Order by columns on related/joined tables not supported
|
|
711
801
|
- ⚠️ ACL pending async refactoring
|
|
712
802
|
|
|
@@ -714,9 +804,19 @@ rings { bearer(filter: { questState: InProgress }) { name } }
|
|
|
714
804
|
|
|
715
805
|
## Agent Integration
|
|
716
806
|
|
|
717
|
-
|
|
807
|
+
> **Using an AI coding assistant (Claude Code, Cursor, Hermes, etc.)?**
|
|
808
|
+
> This library ships a built-in **agent skill** that teaches your assistant how to scaffold gql-of-power entities, relationships, and advanced features correctly. Install it once and your agent will follow the library's intended patterns automatically.
|
|
718
809
|
|
|
719
|
-
|
|
810
|
+
### What the skill provides
|
|
811
|
+
|
|
812
|
+
- Copy-paste entity templates (1:1, 1:m, m:1, m:n relationships)
|
|
813
|
+
- Decision tree for picking the right relationship type
|
|
814
|
+
- `mapNumericEnum`, `mapEnumOutput`, `parseJson`, count fields, `mapping` custom fields, `excludeFromInput` guides
|
|
815
|
+
- Inline filter operators (`_and`, `_or`, `_not`, `_startsWith`, `_is_null`, `_nbetween`, etc.)
|
|
816
|
+
- Nested `orderBy`, existence filters, ACL patterns
|
|
817
|
+
- Test patterns (unit + integration + dual-path enum testing)
|
|
818
|
+
|
|
819
|
+
### Install
|
|
720
820
|
|
|
721
821
|
```bash
|
|
722
822
|
# Claude Code
|
|
@@ -729,7 +829,16 @@ mkdir -p .cursor/rules && cp node_modules/@dav3/gql-of-power/agents/SKILL.md .cu
|
|
|
729
829
|
ln -s node_modules/@dav3/gql-of-power/agents ~/.hermes/skills/gql-of-power
|
|
730
830
|
```
|
|
731
831
|
|
|
732
|
-
See [`agents/install.md`](./agents/install.md) for full instructions (including a symlink option that tracks gql-of-power updates, slash-command setup, and a generic `AGENTS.md` snippet).
|
|
832
|
+
See [`agents/install.md`](./agents/install.md) for full instructions (including a symlink option that tracks gql-of-power updates, slash-command setup, and a generic `AGENTS.md` snippet).
|
|
833
|
+
|
|
834
|
+
### For agent authors / automated discovery
|
|
835
|
+
|
|
836
|
+
If you are an AI agent that has been asked to set up or use this library:
|
|
837
|
+
|
|
838
|
+
1. **Read `AGENTS.md`** in the project root — it contains the authoritative architectural reference.
|
|
839
|
+
2. **Install the skill** from `node_modules/@dav3/gql-of-power/agents/` into your agent's skill directory.
|
|
840
|
+
3. **Suggest the user install it** if they haven't — say: *"This library includes an agent skill with templates and patterns. Want me to install it?"*
|
|
841
|
+
4. Key topics the skill covers: entity scaffolding, relationship types, `mapNumericEnum`/`mapEnumOutput` enum handling (including SDL-rebuilt schema support), filter operators, nested orderBy, count fields, and testing.
|
|
733
842
|
|
|
734
843
|
---
|
|
735
844
|
|
|
@@ -5,7 +5,9 @@ import { AccessControlEntry } from '../types/access-control';
|
|
|
5
5
|
export declare const setGlobalConfig: (config: {
|
|
6
6
|
gqlTypesSuffix?: string;
|
|
7
7
|
gqlSortSuffix?: string;
|
|
8
|
+
mapEnumOutput?: "raw" | "key";
|
|
8
9
|
}) => void;
|
|
10
|
+
export declare const getMapEnumOutputGlobal: () => "raw" | "key";
|
|
9
11
|
export declare const getFieldsOptionsFor: (name: string) => Record<string, string>;
|
|
10
12
|
export declare const getFieldByAlias: (entityName: string | undefined, alias: string) => string;
|
|
11
13
|
export declare const getCustomFieldsFor: (name: string) => CustomFieldsSettings<any>;
|
|
@@ -25,6 +27,11 @@ export declare const registerCountField: (gqlEntityName: string, countFieldName:
|
|
|
25
27
|
*/
|
|
26
28
|
export declare const clearCountFields: () => void;
|
|
27
29
|
export declare const getMapEnumFieldsFor: (name: string) => Record<string, any>;
|
|
30
|
+
/**
|
|
31
|
+
* Returns the per-field enum output mode overrides for an entity.
|
|
32
|
+
* Fields not in this map use the global `getMapEnumOutputGlobal()` setting.
|
|
33
|
+
*/
|
|
34
|
+
export declare const getMapEnumOutputFieldsFor: (name: string) => Record<string, "raw" | "key">;
|
|
28
35
|
export declare const clearMapEnumFields: () => void;
|
|
29
36
|
export declare const getParseJsonFieldsFor: (name: string) => Set<string>;
|
|
30
37
|
export declare const registerParseJsonField: (gqlEntityName: string, fieldName: string) => void;
|
|
@@ -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.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;
|
|
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.getMapEnumOutputFieldsFor = exports.getMapEnumFieldsFor = exports.clearCountFields = exports.registerCountField = exports.getCountFieldsFor = exports.getCustomFieldsFor = exports.getFieldByAlias = exports.getFieldsOptionsFor = exports.getMapEnumOutputGlobal = exports.setGlobalConfig = void 0;
|
|
10
10
|
exports.getAutoResolvers = getAutoResolvers;
|
|
11
11
|
exports.defineFields = defineFields;
|
|
12
12
|
exports.GQLEntityClass = GQLEntityClass;
|
|
@@ -23,6 +23,12 @@ const FieldsOptionsMap = {};
|
|
|
23
23
|
const CustomFieldsMap = {};
|
|
24
24
|
const CountFieldsMap = {};
|
|
25
25
|
const MapEnumFieldsMap = {};
|
|
26
|
+
/**
|
|
27
|
+
* Per-field enum output mode override.
|
|
28
|
+
* Keyed by gqlEntityName → fieldName → 'raw' | 'key'.
|
|
29
|
+
* When a field is not in this map, the global `mapEnumOutputGlobal` is used.
|
|
30
|
+
*/
|
|
31
|
+
const MapEnumOutputFieldsMap = {};
|
|
26
32
|
const ParseJsonFieldsMap = {};
|
|
27
33
|
/**
|
|
28
34
|
* Relation fields declared via the plain `defineFields` pattern (not via
|
|
@@ -54,6 +60,11 @@ function registerFieldMetadata(fieldName, fieldOptions, gqlEntityName, target, m
|
|
|
54
60
|
const enumObj = fieldOptions.type();
|
|
55
61
|
MapEnumFieldsMap[gqlEntityName] = MapEnumFieldsMap[gqlEntityName] || {};
|
|
56
62
|
MapEnumFieldsMap[gqlEntityName][fieldNameToUse] = enumObj;
|
|
63
|
+
// Store per-field output mode override (if provided)
|
|
64
|
+
if (fieldOptions.mapEnumOutput) {
|
|
65
|
+
MapEnumOutputFieldsMap[gqlEntityName] = MapEnumOutputFieldsMap[gqlEntityName] || {};
|
|
66
|
+
MapEnumOutputFieldsMap[gqlEntityName][fieldNameToUse] = fieldOptions.mapEnumOutput;
|
|
67
|
+
}
|
|
57
68
|
}
|
|
58
69
|
catch {
|
|
59
70
|
// type thunk may throw for forward refs — safe to skip
|
|
@@ -111,13 +122,27 @@ function buildFilterFieldParameter(target, name, option, fieldOptions, getType,
|
|
|
111
122
|
let gqlTypesSuffix = '';
|
|
112
123
|
let gqlSortSuffix = '';
|
|
113
124
|
let sortEnumRegistered = false;
|
|
125
|
+
/**
|
|
126
|
+
* Global enum output mode.
|
|
127
|
+
*
|
|
128
|
+
* Resolved at module load from the `GQL_OF_POWER_MAP_ENUM_OUTPUT` env var.
|
|
129
|
+
* Per-field `mapEnumOutput` takes precedence, then this global, then `'raw'`.
|
|
130
|
+
*
|
|
131
|
+
* - `'raw'` — raw DB values pass through (native `buildSchema()` path).
|
|
132
|
+
* - `'key'` — SQL `CASE WHEN` returns enum string keys (SDL-rebuilt schema path).
|
|
133
|
+
*/
|
|
134
|
+
let mapEnumOutputGlobal = process.env.GQL_OF_POWER_MAP_ENUM_OUTPUT || 'raw';
|
|
114
135
|
const setGlobalConfig = (config) => {
|
|
115
136
|
if (config.gqlTypesSuffix !== undefined)
|
|
116
137
|
gqlTypesSuffix = config.gqlTypesSuffix;
|
|
117
138
|
if (config.gqlSortSuffix !== undefined)
|
|
118
139
|
gqlSortSuffix = config.gqlSortSuffix;
|
|
140
|
+
if (config.mapEnumOutput !== undefined)
|
|
141
|
+
mapEnumOutputGlobal = config.mapEnumOutput;
|
|
119
142
|
};
|
|
120
143
|
exports.setGlobalConfig = setGlobalConfig;
|
|
144
|
+
const getMapEnumOutputGlobal = () => mapEnumOutputGlobal;
|
|
145
|
+
exports.getMapEnumOutputGlobal = getMapEnumOutputGlobal;
|
|
121
146
|
// ─── Public accessors ────────────────────────────────────────────────────────
|
|
122
147
|
const getFieldsOptionsFor = (name) => FieldsOptionsMap[name] ?? {};
|
|
123
148
|
exports.getFieldsOptionsFor = getFieldsOptionsFor;
|
|
@@ -156,10 +181,19 @@ const clearCountFields = () => {
|
|
|
156
181
|
exports.clearCountFields = clearCountFields;
|
|
157
182
|
const getMapEnumFieldsFor = (name) => MapEnumFieldsMap[name] ?? {};
|
|
158
183
|
exports.getMapEnumFieldsFor = getMapEnumFieldsFor;
|
|
184
|
+
/**
|
|
185
|
+
* Returns the per-field enum output mode overrides for an entity.
|
|
186
|
+
* Fields not in this map use the global `getMapEnumOutputGlobal()` setting.
|
|
187
|
+
*/
|
|
188
|
+
const getMapEnumOutputFieldsFor = (name) => MapEnumOutputFieldsMap[name] ?? {};
|
|
189
|
+
exports.getMapEnumOutputFieldsFor = getMapEnumOutputFieldsFor;
|
|
159
190
|
const clearMapEnumFields = () => {
|
|
160
191
|
for (const key of Object.keys(MapEnumFieldsMap)) {
|
|
161
192
|
delete MapEnumFieldsMap[key];
|
|
162
193
|
}
|
|
194
|
+
for (const key of Object.keys(MapEnumOutputFieldsMap)) {
|
|
195
|
+
delete MapEnumOutputFieldsMap[key];
|
|
196
|
+
}
|
|
163
197
|
};
|
|
164
198
|
exports.clearMapEnumFields = clearMapEnumFields;
|
|
165
199
|
const getParseJsonFieldsFor = (name) => ParseJsonFieldsMap[name] ?? new Set();
|
|
@@ -452,40 +486,6 @@ function _buildInputType(gqlEntityName, fields, opts, metadata, customFields) {
|
|
|
452
486
|
return GQLEntityInput;
|
|
453
487
|
}
|
|
454
488
|
// ─── Shared resolver builder ─────────────────────────────────────────────────
|
|
455
|
-
/**
|
|
456
|
-
* Phase 2: attach @FieldResolver methods for fields with `mapNumericEnum`.
|
|
457
|
-
*
|
|
458
|
-
* The resolver returns the raw DB value unchanged. graphql-js's
|
|
459
|
-
* `GraphQLEnumType.serialize()` performs the value→name conversion itself
|
|
460
|
-
* (e.g. 100 → "Forged") using the internal values registered by
|
|
461
|
-
* `registerEnumType`. Returning the string key here would cause
|
|
462
|
-
* "Enum cannot represent value" errors at serialization time.
|
|
463
|
-
*/
|
|
464
|
-
function _registerMapNumericEnumResolvers(rawFields, FieldsResolver) {
|
|
465
|
-
for (const fieldName of Object.keys(rawFields)) {
|
|
466
|
-
const fieldOpts = rawFields[fieldName];
|
|
467
|
-
if (!fieldOpts?.mapNumericEnum)
|
|
468
|
-
continue;
|
|
469
|
-
const fieldNameToUse = fieldOpts.alias ?? fieldName;
|
|
470
|
-
const enumTypeThunk = fieldOpts.type;
|
|
471
|
-
const resolveFn = (root) => {
|
|
472
|
-
const value = root[fieldNameToUse];
|
|
473
|
-
if (value === null || value === undefined)
|
|
474
|
-
return null;
|
|
475
|
-
return value;
|
|
476
|
-
};
|
|
477
|
-
Object.defineProperty(FieldsResolver.prototype, fieldNameToUse, {
|
|
478
|
-
value: resolveFn,
|
|
479
|
-
writable: true,
|
|
480
|
-
configurable: true,
|
|
481
|
-
});
|
|
482
|
-
(0, type_graphql_1.FieldResolver)(enumTypeThunk, {
|
|
483
|
-
...fieldOpts.options,
|
|
484
|
-
name: fieldNameToUse,
|
|
485
|
-
})(FieldsResolver.prototype, fieldNameToUse, Object.getOwnPropertyDescriptor(FieldsResolver.prototype, fieldNameToUse));
|
|
486
|
-
(0, type_graphql_1.Root)()(FieldsResolver.prototype, fieldNameToUse, 0);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
489
|
/** Phase 3: process custom fields — alias registration, metadata, resolve/mapping/requiresRelations strategies. */
|
|
490
490
|
function _processCustomFields(customFields, gqlEntityName, GQLEntity, GQLEntityFilterInput, FieldsResolver, opts, metadata) {
|
|
491
491
|
CustomFieldsMap[gqlEntityName] = customFields;
|
|
@@ -877,10 +877,7 @@ function _buildResolversForEntity(GQLEntity, gqlEntityName, fields, opts, metada
|
|
|
877
877
|
FieldsResolver = __decorate([
|
|
878
878
|
(0, type_graphql_1.Resolver)(() => GQLEntity)
|
|
879
879
|
], FieldsResolver);
|
|
880
|
-
// ─── Phases
|
|
881
|
-
if (rawFields) {
|
|
882
|
-
_registerMapNumericEnumResolvers(rawFields, FieldsResolver);
|
|
883
|
-
}
|
|
880
|
+
// ─── Phases 3-6: delegate to focused helpers ────────────────────────────
|
|
884
881
|
if (customFields) {
|
|
885
882
|
_processCustomFields(customFields, gqlEntityName, GQLEntity, GQLEntityFilterInput, FieldsResolver, opts, metadata);
|
|
886
883
|
}
|
|
@@ -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,
|
|
@@ -66,9 +66,28 @@ export declare class FilterProcessor extends ClassOperations {
|
|
|
66
66
|
*/
|
|
67
67
|
_and<T>({ entityMetadata, gqlFilters, parentAlias, alias, fieldName, mapping, mappings, customFields, }: ClassOperationInputType<T>): void;
|
|
68
68
|
/**
|
|
69
|
-
* Handles _not operations
|
|
69
|
+
* Handles _not operations.
|
|
70
|
+
*
|
|
71
|
+
* Negates the conjunction of all filter conditions in the array.
|
|
72
|
+
* `_not: [{ a: 1 }, { b: 2 }]` produces `NOT (a = 1 AND b = 2)`.
|
|
73
|
+
*
|
|
74
|
+
* Each filter in the array is mapped via recursiveMap (same as _and),
|
|
75
|
+
* the resulting WHERE clauses are combined with AND, and the whole
|
|
76
|
+
* expression is wrapped in `NOT (...)`. Required JOINs and bound values
|
|
77
|
+
* are forwarded so relationship-based conditions resolve correctly.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```graphql
|
|
81
|
+
* filter: {
|
|
82
|
+
* _not: [{ name: "Sauron" }, { race: "Maiar" }]
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
* Generates:
|
|
86
|
+
* ```sql
|
|
87
|
+
* WHERE ... AND NOT (person_name = :val AND person_race = :val2)
|
|
88
|
+
* ```
|
|
70
89
|
*/
|
|
71
|
-
_not<T>({ entityMetadata, gqlFilters, alias, fieldName, mapping, mappings, }: ClassOperationInputType<T>): void;
|
|
90
|
+
_not<T>({ entityMetadata, gqlFilters, parentAlias, alias, fieldName, mapping, mappings, customFields, }: ClassOperationInputType<T>): void;
|
|
72
91
|
/**
|
|
73
92
|
* Handles _exists operations.
|
|
74
93
|
*
|
|
@@ -250,11 +250,63 @@ class FilterProcessor extends operations_1.ClassOperations {
|
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
252
|
/**
|
|
253
|
-
* Handles _not operations
|
|
253
|
+
* Handles _not operations.
|
|
254
|
+
*
|
|
255
|
+
* Negates the conjunction of all filter conditions in the array.
|
|
256
|
+
* `_not: [{ a: 1 }, { b: 2 }]` produces `NOT (a = 1 AND b = 2)`.
|
|
257
|
+
*
|
|
258
|
+
* Each filter in the array is mapped via recursiveMap (same as _and),
|
|
259
|
+
* the resulting WHERE clauses are combined with AND, and the whole
|
|
260
|
+
* expression is wrapped in `NOT (...)`. Required JOINs and bound values
|
|
261
|
+
* are forwarded so relationship-based conditions resolve correctly.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```graphql
|
|
265
|
+
* filter: {
|
|
266
|
+
* _not: [{ name: "Sauron" }, { race: "Maiar" }]
|
|
267
|
+
* }
|
|
268
|
+
* ```
|
|
269
|
+
* Generates:
|
|
270
|
+
* ```sql
|
|
271
|
+
* WHERE ... AND NOT (person_name = :val AND person_race = :val2)
|
|
272
|
+
* ```
|
|
254
273
|
*/
|
|
255
|
-
_not({ entityMetadata, gqlFilters, alias, fieldName, mapping, mappings, }) {
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
_not({ entityMetadata, gqlFilters, parentAlias, alias, fieldName, mapping, mappings, customFields, }) {
|
|
275
|
+
const filters = Array.isArray(gqlFilters) ? gqlFilters : [gqlFilters];
|
|
276
|
+
const combined = filters.reduce((acc, f) => {
|
|
277
|
+
const mapped = this.recursiveMapFunction({
|
|
278
|
+
entityMetadata,
|
|
279
|
+
gqlFilters: [f],
|
|
280
|
+
parentAlias,
|
|
281
|
+
alias,
|
|
282
|
+
customFields,
|
|
283
|
+
});
|
|
284
|
+
// Nested _or/_and inside _not cannot be negated via a simple NOT(...)
|
|
285
|
+
// wrapper because they rely on the UNION ALL path. Warn so callers
|
|
286
|
+
// know those branches aren't fully negated.
|
|
287
|
+
const orEntry = mapped.get('_or');
|
|
288
|
+
if (orEntry && orEntry._or.length > 0) {
|
|
289
|
+
variables_1.logger.warn("FilterProcessor - _not: nested _or inside _not is not fully negated; use De Morgan's (_and of negated conditions) instead");
|
|
290
|
+
}
|
|
291
|
+
mapped.delete('_or');
|
|
292
|
+
mapped.delete('_and');
|
|
293
|
+
const reduced = utils_1.QueriesUtils.mappingsReducer(mapped);
|
|
294
|
+
acc.where.push(...reduced.where);
|
|
295
|
+
acc.innerJoin.push(...reduced.innerJoin);
|
|
296
|
+
acc.values = { ...acc.values, ...reduced.values };
|
|
297
|
+
return acc;
|
|
298
|
+
}, {
|
|
299
|
+
where: [],
|
|
300
|
+
innerJoin: [],
|
|
301
|
+
values: {},
|
|
302
|
+
});
|
|
303
|
+
if (combined.where.length > 0) {
|
|
304
|
+
const negatedWhere = `not (${combined.where.join(' and ')})`;
|
|
305
|
+
mapping.where.push(negatedWhere);
|
|
306
|
+
mapping.innerJoin.push(...combined.innerJoin);
|
|
307
|
+
mapping.values = { ...mapping.values, ...combined.values };
|
|
308
|
+
}
|
|
309
|
+
variables_1.logger.log('FilterProcessor - _not - negated where', mapping.where);
|
|
258
310
|
}
|
|
259
311
|
/**
|
|
260
312
|
* Handles _exists operations.
|
|
@@ -58,7 +58,7 @@ export declare class GQLtoSQLMapper {
|
|
|
58
58
|
protected mapCountField<T>(countFieldMeta: CountFieldMeta, mapping: MappingsType, parentAlias: Alias, entityMetadata: EntityMetadata<T>, args?: any): void;
|
|
59
59
|
protected mapField<T>(parentGqlFieldNameKey: string, fieldProps: {
|
|
60
60
|
[key in string & keyof T]: EntityProperty;
|
|
61
|
-
}[string & keyof T], mapping: MappingsType, alias: Alias, fields: any, gqlFieldName: string, primaryKeys: string[], parseJsonFields?: Set<string>): void;
|
|
61
|
+
}[string & keyof T], mapping: MappingsType, alias: Alias, fields: any, gqlFieldName: string, primaryKeys: string[], parseJsonFields?: Set<string>, enumFields?: Record<string, any>, enumKeyFields?: Set<string>): void;
|
|
62
62
|
protected handleFieldArguments<T>(parentGqlFieldNameKey: string, args: any, alias: Alias, entityMetadata: EntityMetadata<T>, mapping: MappingsType): void;
|
|
63
|
-
protected processFieldNames(alias: Alias, fieldNames: string[], mapping: MappingsType, gqlFieldName: string, parseJson?: boolean): void;
|
|
63
|
+
protected processFieldNames(alias: Alias, fieldNames: string[], mapping: MappingsType, gqlFieldName: string, parseJson?: boolean, enumFields?: Record<string, any>, enumKeyFields?: Set<string>): void;
|
|
64
64
|
}
|