@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 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` | ILIKE (case-insensitive contains) |
390
- | `_gt` / `_gte` | Greater than / greater than or equal |
391
- | `_lt` / `_lte` | Less than / less than or equal |
392
- | `_and` | Logical AND |
393
- | `_or` | Logical OR (generates UNION ALL) |
394
- | `_exists` | Check related entities exist (AND-combined per key) |
395
- | `_not_exists` | Check no related entities exist (AND-combined per key) |
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
- This library ships an **agent skill** (`agents/`) that teaches AI coding assistants (Claude Code, Cursor, Hermes, or any agent reading `AGENTS.md`) how to scaffold gql-of-power entities, relationships, and advanced features correctly — using copy-paste templates, annotated relationship examples, and a decision tree for picking the right relationship type.
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
- To install it in a consumer project (one-time setup):
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). Once installed, ask your agent to "add a gql-of-power entity" and it will follow the library's intended patterns.
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 2-6: delegate to focused helpers ────────────────────────────
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 _re: T;
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
- _re: ([l, r]: string[], []: Scalar[]) => {
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;
@@ -64,14 +64,42 @@ exports.FieldOperations = {
64
64
  where: `${l} like ${r}`,
65
65
  value: undefined,
66
66
  }),
67
- _re: ([l, r], []) => ({
68
- where: `${l} ~ ${r}`,
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 (placeholder implementation)
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 (placeholder implementation)
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
- // TODO: Implement _not operation
257
- variables_1.logger.warn('FilterProcessor - _not operation not yet implemented');
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
  }