@dav3/gql-of-power 1.6.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 +78 -3
- package/dist/entities/gql-entity.d.ts +7 -0
- package/dist/entities/gql-entity.js +35 -1
- package/dist/queries/gql-to-sql-mapper.d.ts +2 -2
- package/dist/queries/gql-to-sql-mapper.js +28 -4
- 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 +42 -1
- package/src/queries/gql-to-sql-mapper.ts +42 -7
- package/src/queries/sql-builder.ts +33 -0
- package/src/types/sql-types.ts +27 -0
package/README.md
CHANGED
|
@@ -561,6 +561,7 @@ pagination: {
|
|
|
561
561
|
| `D3GOP_LOG_TYPE` | Logging level: `debug` or `disabled` |
|
|
562
562
|
| `D3GOP_DEFAULT_QUERY_LIMIT` | Default query limit when pagination is not specified (default: `3000`) |
|
|
563
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. |
|
|
564
565
|
|
|
565
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.
|
|
566
567
|
|
|
@@ -574,6 +575,9 @@ setGlobalConfig({ gqlTypesSuffix: 'V2' });
|
|
|
574
575
|
|
|
575
576
|
// Optionally set a separate suffix for sort/pagination types
|
|
576
577
|
setGlobalConfig({ gqlTypesSuffix: 'V2', gqlSortSuffix: 'V2' });
|
|
578
|
+
|
|
579
|
+
// Enum output mode for SDL-rebuilt schemas (Apollo Server + .graphql file)
|
|
580
|
+
setGlobalConfig({ mapEnumOutput: 'key' });
|
|
577
581
|
```
|
|
578
582
|
|
|
579
583
|
---
|
|
@@ -739,6 +743,58 @@ rings { bearer(filter: { questState: InProgress }) { name } }
|
|
|
739
743
|
|
|
740
744
|
---
|
|
741
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
|
+
|
|
742
798
|
## Known Limitations
|
|
743
799
|
|
|
744
800
|
- ⚠️ Order by columns on related/joined tables not supported
|
|
@@ -748,9 +804,19 @@ rings { bearer(filter: { questState: InProgress }) { name } }
|
|
|
748
804
|
|
|
749
805
|
## Agent Integration
|
|
750
806
|
|
|
751
|
-
|
|
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.
|
|
809
|
+
|
|
810
|
+
### What the skill provides
|
|
752
811
|
|
|
753
|
-
|
|
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
|
|
754
820
|
|
|
755
821
|
```bash
|
|
756
822
|
# Claude Code
|
|
@@ -763,7 +829,16 @@ mkdir -p .cursor/rules && cp node_modules/@dav3/gql-of-power/agents/SKILL.md .cu
|
|
|
763
829
|
ln -s node_modules/@dav3/gql-of-power/agents ~/.hermes/skills/gql-of-power
|
|
764
830
|
```
|
|
765
831
|
|
|
766
|
-
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.
|
|
767
842
|
|
|
768
843
|
---
|
|
769
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();
|
|
@@ -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
|
}
|
|
@@ -117,6 +117,18 @@ class GQLtoSQLMapper {
|
|
|
117
117
|
},
|
|
118
118
|
}, definedFields) ?? definedFields;
|
|
119
119
|
const parseJsonFields = (0, gql_entity_1.getParseJsonFieldsFor)((0, gql_entity_1.getGQLEntityNameFor)(entityMetadata.name ?? ''));
|
|
120
|
+
const enumFields = (0, gql_entity_1.getMapEnumFieldsFor)((0, gql_entity_1.getGQLEntityNameFor)(entityMetadata.name ?? ''));
|
|
121
|
+
const enumOutputOverrides = (0, gql_entity_1.getMapEnumOutputFieldsFor)((0, gql_entity_1.getGQLEntityNameFor)(entityMetadata.name ?? ''));
|
|
122
|
+
// Build a set of field names that should use CASE WHEN (key mode).
|
|
123
|
+
// A field uses 'key' mode if: per-field override is 'key', OR no override
|
|
124
|
+
// and the global default is 'key'.
|
|
125
|
+
const globalEnumOutput = (0, gql_entity_1.getMapEnumOutputGlobal)();
|
|
126
|
+
const enumKeyFields = new Set();
|
|
127
|
+
for (const fieldName of Object.keys(enumFields)) {
|
|
128
|
+
const mode = enumOutputOverrides[fieldName] ?? globalEnumOutput;
|
|
129
|
+
if (mode === 'key')
|
|
130
|
+
enumKeyFields.add(fieldName);
|
|
131
|
+
}
|
|
120
132
|
let res = (0, object_1.keys)(allFields).reduce(({ mappings }, gqlFieldNameKey) => {
|
|
121
133
|
variables_1.logger.log('========================== FIELD ==========================', {
|
|
122
134
|
gqlFieldNameKey,
|
|
@@ -165,7 +177,7 @@ class GQLtoSQLMapper {
|
|
|
165
177
|
return this.mapCustomField(customFieldProps, mapping, alias, gqlFieldName, mappings, fieldsByTypeName, entityMetadata, args);
|
|
166
178
|
}
|
|
167
179
|
else {
|
|
168
|
-
this.mapField(fieldName, fieldProps, mapping, alias, fieldsByTypeName, gqlFieldName, primaryKeys, parseJsonFields);
|
|
180
|
+
this.mapField(fieldName, fieldProps, mapping, alias, fieldsByTypeName, gqlFieldName, primaryKeys, parseJsonFields, enumFields, enumKeyFields);
|
|
169
181
|
// gqlFieldName === 'battles' &&
|
|
170
182
|
variables_1.logger.log('=======', { tableName, gqlFieldName, fieldName, name }, (0, gql_to_sql_types_1.mappingsTypeToString)(mapping, true));
|
|
171
183
|
variables_1.logger.log('');
|
|
@@ -474,7 +486,7 @@ class GQLtoSQLMapper {
|
|
|
474
486
|
mapping.values = { ...mapping.values, ...filterValues };
|
|
475
487
|
variables_1.logger.log('mapCountField', countFieldName, 'subquery', subquery);
|
|
476
488
|
}
|
|
477
|
-
mapField(parentGqlFieldNameKey, fieldProps, mapping, alias, fields, gqlFieldName, primaryKeys, parseJsonFields = new Set()) {
|
|
489
|
+
mapField(parentGqlFieldNameKey, fieldProps, mapping, alias, fields, gqlFieldName, primaryKeys, parseJsonFields = new Set(), enumFields = {}, enumKeyFields = new Set()) {
|
|
478
490
|
const referenceField = this.exists(fieldProps.type) && this.getMetadata(fieldProps.type);
|
|
479
491
|
variables_1.logger.warn('[DIAG mapField]', gqlFieldName, 'fieldProps.type:', fieldProps.type, 'exists:', this.exists(fieldProps.type), 'reference found:', !!referenceField, 'reference:', fieldProps.reference, 'fieldNames:', fieldProps.fieldNames);
|
|
480
492
|
if (referenceField) {
|
|
@@ -542,7 +554,7 @@ class GQLtoSQLMapper {
|
|
|
542
554
|
}
|
|
543
555
|
else if (fieldProps.fieldNames.length > 0) {
|
|
544
556
|
variables_1.logger.log('MAPPING for FIELDS:', { parentGqlFieldNameKey, gqlFieldName }, (0, gql_to_sql_types_1.mappingsTypeToString)(mapping, true));
|
|
545
|
-
this.processFieldNames(alias, fieldProps.fieldNames, mapping, gqlFieldName, parseJsonFields.has(gqlFieldName));
|
|
557
|
+
this.processFieldNames(alias, fieldProps.fieldNames, mapping, gqlFieldName, parseJsonFields.has(gqlFieldName), enumFields, enumKeyFields);
|
|
546
558
|
}
|
|
547
559
|
else {
|
|
548
560
|
variables_1.logger.log('reference type', fieldProps.reference, 'not handled for field', gqlFieldName);
|
|
@@ -598,7 +610,7 @@ class GQLtoSQLMapper {
|
|
|
598
610
|
}
|
|
599
611
|
// mappings.set(parentGqlFieldNameKey, m);
|
|
600
612
|
}
|
|
601
|
-
processFieldNames(alias, fieldNames, mapping, gqlFieldName, parseJson = false) {
|
|
613
|
+
processFieldNames(alias, fieldNames, mapping, gqlFieldName, parseJson = false, enumFields = {}, enumKeyFields = new Set()) {
|
|
602
614
|
variables_1.logger.info('GQLtoSQLMapper - processFieldNames', fieldNames, gqlFieldName);
|
|
603
615
|
if (fieldNames.length <= 0) {
|
|
604
616
|
variables_1.logger.warn('GQLtoSQLMapper - processFieldNames: fieldNames is empty', gqlFieldName, 'skipping');
|
|
@@ -612,6 +624,18 @@ class GQLtoSQLMapper {
|
|
|
612
624
|
const jsonExpr = `REPLACE(TRIM(BOTH '"' FROM ${fieldNameWithAlias}::text), '${'\\"'}','"')::jsonb`;
|
|
613
625
|
aliasedField = `${jsonExpr} AS "${gqlFieldName}"`;
|
|
614
626
|
}
|
|
627
|
+
else if (gqlFieldName in enumFields && enumKeyFields.has(gqlFieldName)) {
|
|
628
|
+
// CASE WHEN mode: SQL returns the enum string key directly.
|
|
629
|
+
// Used when the schema is rebuilt from SDL (Apollo Server with
|
|
630
|
+
// pre-generated schema file), where graphql-js enum values default
|
|
631
|
+
// to the name string instead of the numeric value.
|
|
632
|
+
const caseExpr = sql_builder_1.SQLBuilder.buildEnumCaseSQL(fieldNameWithAlias, enumFields[gqlFieldName]);
|
|
633
|
+
aliasedField = caseExpr
|
|
634
|
+
? `${caseExpr} AS "${gqlFieldName}"`
|
|
635
|
+
: gqlFieldName !== fieldNames[0]
|
|
636
|
+
? `${fieldNameWithAlias} AS "${gqlFieldName}"`
|
|
637
|
+
: fieldNameWithAlias;
|
|
638
|
+
}
|
|
615
639
|
else {
|
|
616
640
|
aliasedField =
|
|
617
641
|
gqlFieldName !== fieldNames[0]
|
|
@@ -46,6 +46,26 @@ export declare class SQLBuilder {
|
|
|
46
46
|
where: string;
|
|
47
47
|
}) => string): string[];
|
|
48
48
|
static buildInnerBranch(rawSelect: string[], tableName: string, alias: Alias, innerJoin: string[], whereConditions: string[]): string;
|
|
49
|
+
/**
|
|
50
|
+
* Builds a SQL CASE expression that maps a raw DB enum column value to its
|
|
51
|
+
* enum string key (name). Used for `mapNumericEnum` fields so the query
|
|
52
|
+
* returns the GraphQL-ready string directly — no post-query conversion.
|
|
53
|
+
*
|
|
54
|
+
* Iterates the enum object's keys. Numeric-valued keys produce a numeric
|
|
55
|
+
* WHEN branch; string-valued keys produce a quoted-string WHEN branch.
|
|
56
|
+
*
|
|
57
|
+
* Example output:
|
|
58
|
+
* CASE "a1"."state"
|
|
59
|
+
* WHEN 0 THEN 'NotStarted'
|
|
60
|
+
* WHEN 1 THEN 'InProgress'
|
|
61
|
+
* ELSE NULL
|
|
62
|
+
* END
|
|
63
|
+
*
|
|
64
|
+
* @param columnExpr Raw SQL column expression (e.g. `"a1"."state"`)
|
|
65
|
+
* @param enumObj The TypeScript enum object (e.g. `QuestState`)
|
|
66
|
+
* @returns CASE WHEN ... THEN ... END expression, or null if no values
|
|
67
|
+
*/
|
|
68
|
+
static buildEnumCaseSQL(columnExpr: string, enumObj: Record<string, any>): string | null;
|
|
49
69
|
static getFieldMapper: <T>(metadata: EntityMetadata<T>, alias: Alias) => (ob: string) => string[];
|
|
50
70
|
/**
|
|
51
71
|
* Builds ORDER BY SQL clause from pagination input
|
|
@@ -73,6 +73,39 @@ class SQLBuilder {
|
|
|
73
73
|
where true
|
|
74
74
|
${whereConditions.length > 0 ? ` and ( ${whereConditions.join(' and ')} )` : ''}`.replaceAll(/[ \n\t]+/gi, ' ');
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds a SQL CASE expression that maps a raw DB enum column value to its
|
|
78
|
+
* enum string key (name). Used for `mapNumericEnum` fields so the query
|
|
79
|
+
* returns the GraphQL-ready string directly — no post-query conversion.
|
|
80
|
+
*
|
|
81
|
+
* Iterates the enum object's keys. Numeric-valued keys produce a numeric
|
|
82
|
+
* WHEN branch; string-valued keys produce a quoted-string WHEN branch.
|
|
83
|
+
*
|
|
84
|
+
* Example output:
|
|
85
|
+
* CASE "a1"."state"
|
|
86
|
+
* WHEN 0 THEN 'NotStarted'
|
|
87
|
+
* WHEN 1 THEN 'InProgress'
|
|
88
|
+
* ELSE NULL
|
|
89
|
+
* END
|
|
90
|
+
*
|
|
91
|
+
* @param columnExpr Raw SQL column expression (e.g. `"a1"."state"`)
|
|
92
|
+
* @param enumObj The TypeScript enum object (e.g. `QuestState`)
|
|
93
|
+
* @returns CASE WHEN ... THEN ... END expression, or null if no values
|
|
94
|
+
*/
|
|
95
|
+
static buildEnumCaseSQL(columnExpr, enumObj) {
|
|
96
|
+
const branches = [];
|
|
97
|
+
for (const key of Object.keys(enumObj)) {
|
|
98
|
+
// Skip TypeScript's numeric reverse-mapping entries
|
|
99
|
+
if (/^\d+$/.test(key))
|
|
100
|
+
continue;
|
|
101
|
+
const val = enumObj[key];
|
|
102
|
+
const sqlVal = typeof val === 'number' ? val : `'${String(val).replace(/'/g, "''")}'`;
|
|
103
|
+
branches.push(`WHEN ${sqlVal} THEN '${key}'`);
|
|
104
|
+
}
|
|
105
|
+
if (branches.length === 0)
|
|
106
|
+
return null;
|
|
107
|
+
return `CASE ${columnExpr} ${branches.join(' ')} ELSE NULL END`;
|
|
108
|
+
}
|
|
76
109
|
static getFieldMapper = (metadata, alias) => (ob) => {
|
|
77
110
|
const fieldMeta = metadata.properties[ob];
|
|
78
111
|
if (!fieldMeta) {
|
|
@@ -68,6 +68,33 @@ export type FieldBaseSettings = {
|
|
|
68
68
|
* status: { type: () => Status, mapNumericEnum: true, generateFilter: true }
|
|
69
69
|
*/
|
|
70
70
|
mapNumericEnum?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Controls how enum field values are emitted in the SQL query for GraphQL
|
|
73
|
+
* output serialization. Only takes effect when `mapNumericEnum` is also
|
|
74
|
+
* `true`.
|
|
75
|
+
*
|
|
76
|
+
* - `'raw'` (default) — the raw DB value (e.g. `0`, `100`) flows through
|
|
77
|
+
* untouched. Use this when the schema is built **live** via type-graphql's
|
|
78
|
+
* `buildSchema()`, which preserves numeric enum values so graphql-js's
|
|
79
|
+
* `serialize(0)` resolves to `"ACTIVE"` natively.
|
|
80
|
+
* - `'key'` — wraps the column in a `CASE WHEN ... THEN 'Active' END` SQL
|
|
81
|
+
* expression so the query returns the enum string key directly. Use this
|
|
82
|
+
* when the schema is **rebuilt from SDL** (e.g. `printSchema()` →
|
|
83
|
+
* `buildASTSchema()` / Apollo Server with a pre-generated `.graphql` file),
|
|
84
|
+
* because SDL strips numeric values and `serialize(0)` fails.
|
|
85
|
+
*
|
|
86
|
+
* Can also be set globally via `setGlobalConfig({ mapEnumOutput: 'key' })`
|
|
87
|
+
* or the `GQL_OF_POWER_MAP_ENUM_OUTPUT=key` environment variable.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* stateCode: {
|
|
91
|
+
* type: () => StateCode,
|
|
92
|
+
* mapNumericEnum: true,
|
|
93
|
+
* mapEnumOutput: 'key',
|
|
94
|
+
* generateFilter: true,
|
|
95
|
+
* }
|
|
96
|
+
*/
|
|
97
|
+
mapEnumOutput?: 'raw' | 'key';
|
|
71
98
|
/**
|
|
72
99
|
* When true, the SQL query wraps this field's column with a JSON parsing
|
|
73
100
|
* expression so the value is returned as a JSON object instead of a raw
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dav3/gql-of-power",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Generate a single optimized SQL query from GraphQL operations — eliminate N+1 with recursive field-to-SQL mapping.",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Generate a single optimized SQL query from GraphQL operations — eliminate N+1 with recursive field-to-SQL mapping. Ships with agent skill for AI coding assistants.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"module": "esnext",
|
|
@@ -30,10 +30,22 @@
|
|
|
30
30
|
"audit:health": "fallow health",
|
|
31
31
|
"audit:fix": "fallow fix"
|
|
32
32
|
},
|
|
33
|
-
"keywords": [
|
|
33
|
+
"keywords": [
|
|
34
|
+
"graphql",
|
|
35
|
+
"sql",
|
|
36
|
+
"type-graphql",
|
|
37
|
+
"n-plus-1",
|
|
38
|
+
"agent-skill",
|
|
39
|
+
"claude-code",
|
|
40
|
+
"cursor",
|
|
41
|
+
"orm"
|
|
42
|
+
],
|
|
34
43
|
"author": "",
|
|
35
44
|
"license": "MIT NON-AI License",
|
|
36
45
|
"devDependencies": {
|
|
46
|
+
"@apollo/server": "^5.5.1",
|
|
47
|
+
"@as-integrations/aws-lambda": "^4.1.0",
|
|
48
|
+
"@graphql-tools/schema": "^10.0.38",
|
|
37
49
|
"@types/bun": "latest",
|
|
38
50
|
"@types/graphql-fields": "^1.3.9",
|
|
39
51
|
"fallow": "^3.6.0",
|
|
@@ -33,6 +33,13 @@ const FieldsOptionsMap: Record<string, Record<string, string>> = {};
|
|
|
33
33
|
const CustomFieldsMap: Record<string, CustomFieldsSettings<any>> = {};
|
|
34
34
|
const CountFieldsMap: Record<string, Record<string, CountFieldMeta>> = {};
|
|
35
35
|
const MapEnumFieldsMap: Record<string, Record<string, any>> = {};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-field enum output mode override.
|
|
39
|
+
* Keyed by gqlEntityName → fieldName → 'raw' | 'key'.
|
|
40
|
+
* When a field is not in this map, the global `mapEnumOutputGlobal` is used.
|
|
41
|
+
*/
|
|
42
|
+
const MapEnumOutputFieldsMap: Record<string, Record<string, 'raw' | 'key'>> = {};
|
|
36
43
|
const ParseJsonFieldsMap: Record<string, Set<string>> = {};
|
|
37
44
|
/**
|
|
38
45
|
* Relation fields declared via the plain `defineFields` pattern (not via
|
|
@@ -73,6 +80,11 @@ function registerFieldMetadata(
|
|
|
73
80
|
const enumObj = fieldOptions.type();
|
|
74
81
|
MapEnumFieldsMap[gqlEntityName] = MapEnumFieldsMap[gqlEntityName] || {};
|
|
75
82
|
MapEnumFieldsMap[gqlEntityName][fieldNameToUse] = enumObj;
|
|
83
|
+
// Store per-field output mode override (if provided)
|
|
84
|
+
if (fieldOptions.mapEnumOutput) {
|
|
85
|
+
MapEnumOutputFieldsMap[gqlEntityName] = MapEnumOutputFieldsMap[gqlEntityName] || {};
|
|
86
|
+
MapEnumOutputFieldsMap[gqlEntityName][fieldNameToUse] = fieldOptions.mapEnumOutput;
|
|
87
|
+
}
|
|
76
88
|
} catch {
|
|
77
89
|
// type thunk may throw for forward refs — safe to skip
|
|
78
90
|
}
|
|
@@ -143,11 +155,30 @@ let gqlTypesSuffix = '';
|
|
|
143
155
|
let gqlSortSuffix = '';
|
|
144
156
|
let sortEnumRegistered = false;
|
|
145
157
|
|
|
146
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Global enum output mode.
|
|
160
|
+
*
|
|
161
|
+
* Resolved at module load from the `GQL_OF_POWER_MAP_ENUM_OUTPUT` env var.
|
|
162
|
+
* Per-field `mapEnumOutput` takes precedence, then this global, then `'raw'`.
|
|
163
|
+
*
|
|
164
|
+
* - `'raw'` — raw DB values pass through (native `buildSchema()` path).
|
|
165
|
+
* - `'key'` — SQL `CASE WHEN` returns enum string keys (SDL-rebuilt schema path).
|
|
166
|
+
*/
|
|
167
|
+
let mapEnumOutputGlobal: 'raw' | 'key' =
|
|
168
|
+
(process.env.GQL_OF_POWER_MAP_ENUM_OUTPUT as 'raw' | 'key') || 'raw';
|
|
169
|
+
|
|
170
|
+
export const setGlobalConfig = (config: {
|
|
171
|
+
gqlTypesSuffix?: string;
|
|
172
|
+
gqlSortSuffix?: string;
|
|
173
|
+
mapEnumOutput?: 'raw' | 'key';
|
|
174
|
+
}) => {
|
|
147
175
|
if (config.gqlTypesSuffix !== undefined) gqlTypesSuffix = config.gqlTypesSuffix;
|
|
148
176
|
if (config.gqlSortSuffix !== undefined) gqlSortSuffix = config.gqlSortSuffix;
|
|
177
|
+
if (config.mapEnumOutput !== undefined) mapEnumOutputGlobal = config.mapEnumOutput;
|
|
149
178
|
};
|
|
150
179
|
|
|
180
|
+
export const getMapEnumOutputGlobal = () => mapEnumOutputGlobal;
|
|
181
|
+
|
|
151
182
|
// ─── Public accessors ────────────────────────────────────────────────────────
|
|
152
183
|
|
|
153
184
|
export const getFieldsOptionsFor = (name: string): Record<string, string> =>
|
|
@@ -193,10 +224,20 @@ export const clearCountFields = (): void => {
|
|
|
193
224
|
export const getMapEnumFieldsFor = (name: string): Record<string, any> =>
|
|
194
225
|
MapEnumFieldsMap[name] ?? {};
|
|
195
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Returns the per-field enum output mode overrides for an entity.
|
|
229
|
+
* Fields not in this map use the global `getMapEnumOutputGlobal()` setting.
|
|
230
|
+
*/
|
|
231
|
+
export const getMapEnumOutputFieldsFor = (name: string): Record<string, 'raw' | 'key'> =>
|
|
232
|
+
MapEnumOutputFieldsMap[name] ?? {};
|
|
233
|
+
|
|
196
234
|
export const clearMapEnumFields = (): void => {
|
|
197
235
|
for (const key of Object.keys(MapEnumFieldsMap)) {
|
|
198
236
|
delete MapEnumFieldsMap[key];
|
|
199
237
|
}
|
|
238
|
+
for (const key of Object.keys(MapEnumOutputFieldsMap)) {
|
|
239
|
+
delete MapEnumOutputFieldsMap[key];
|
|
240
|
+
}
|
|
200
241
|
};
|
|
201
242
|
|
|
202
243
|
export const getParseJsonFieldsFor = (name: string): Set<string> =>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getCountFieldsFor,
|
|
3
2
|
getCustomFieldsFor,
|
|
4
|
-
|
|
5
|
-
getGQLEntityNameFor,
|
|
3
|
+
getCountFieldsFor,
|
|
6
4
|
getMapEnumFieldsFor,
|
|
5
|
+
getMapEnumOutputFieldsFor,
|
|
6
|
+
getMapEnumOutputGlobal,
|
|
7
7
|
getParseJsonFieldsFor,
|
|
8
|
+
getFieldsOptionsFor,
|
|
9
|
+
getFieldByAlias,
|
|
10
|
+
getGQLEntityNameFor,
|
|
8
11
|
getRelationFieldsFor,
|
|
9
12
|
} from '../entities/gql-entity';
|
|
10
13
|
import {
|
|
@@ -252,6 +255,19 @@ export class GQLtoSQLMapper {
|
|
|
252
255
|
) ?? definedFields;
|
|
253
256
|
|
|
254
257
|
const parseJsonFields = getParseJsonFieldsFor(getGQLEntityNameFor(entityMetadata.name ?? ''));
|
|
258
|
+
const enumFields = getMapEnumFieldsFor(getGQLEntityNameFor(entityMetadata.name ?? ''));
|
|
259
|
+
const enumOutputOverrides = getMapEnumOutputFieldsFor(
|
|
260
|
+
getGQLEntityNameFor(entityMetadata.name ?? '')
|
|
261
|
+
);
|
|
262
|
+
// Build a set of field names that should use CASE WHEN (key mode).
|
|
263
|
+
// A field uses 'key' mode if: per-field override is 'key', OR no override
|
|
264
|
+
// and the global default is 'key'.
|
|
265
|
+
const globalEnumOutput = getMapEnumOutputGlobal();
|
|
266
|
+
const enumKeyFields = new Set<string>();
|
|
267
|
+
for (const fieldName of Object.keys(enumFields)) {
|
|
268
|
+
const mode = enumOutputOverrides[fieldName] ?? globalEnumOutput;
|
|
269
|
+
if (mode === 'key') enumKeyFields.add(fieldName);
|
|
270
|
+
}
|
|
255
271
|
|
|
256
272
|
let res = keys(allFields).reduce(
|
|
257
273
|
({ mappings }, gqlFieldNameKey) => {
|
|
@@ -343,7 +359,9 @@ export class GQLtoSQLMapper {
|
|
|
343
359
|
fieldsByTypeName,
|
|
344
360
|
gqlFieldName,
|
|
345
361
|
primaryKeys,
|
|
346
|
-
parseJsonFields
|
|
362
|
+
parseJsonFields,
|
|
363
|
+
enumFields,
|
|
364
|
+
enumKeyFields
|
|
347
365
|
);
|
|
348
366
|
// gqlFieldName === 'battles' &&
|
|
349
367
|
logger.log(
|
|
@@ -927,7 +945,9 @@ export class GQLtoSQLMapper {
|
|
|
927
945
|
fields: any,
|
|
928
946
|
gqlFieldName: string,
|
|
929
947
|
primaryKeys: string[],
|
|
930
|
-
parseJsonFields: Set<string> = new Set()
|
|
948
|
+
parseJsonFields: Set<string> = new Set(),
|
|
949
|
+
enumFields: Record<string, any> = {},
|
|
950
|
+
enumKeyFields: Set<string> = new Set()
|
|
931
951
|
) {
|
|
932
952
|
const referenceField =
|
|
933
953
|
this.exists(fieldProps.type) && this.getMetadata<any, EntityMetadata<any>>(fieldProps.type);
|
|
@@ -1137,7 +1157,9 @@ export class GQLtoSQLMapper {
|
|
|
1137
1157
|
fieldProps.fieldNames,
|
|
1138
1158
|
mapping,
|
|
1139
1159
|
gqlFieldName,
|
|
1140
|
-
parseJsonFields.has(gqlFieldName)
|
|
1160
|
+
parseJsonFields.has(gqlFieldName),
|
|
1161
|
+
enumFields,
|
|
1162
|
+
enumKeyFields
|
|
1141
1163
|
);
|
|
1142
1164
|
} else {
|
|
1143
1165
|
logger.log('reference type', fieldProps.reference, 'not handled for field', gqlFieldName);
|
|
@@ -1221,7 +1243,9 @@ export class GQLtoSQLMapper {
|
|
|
1221
1243
|
fieldNames: string[],
|
|
1222
1244
|
mapping: MappingsType,
|
|
1223
1245
|
gqlFieldName: string,
|
|
1224
|
-
parseJson: boolean = false
|
|
1246
|
+
parseJson: boolean = false,
|
|
1247
|
+
enumFields: Record<string, any> = {},
|
|
1248
|
+
enumKeyFields: Set<string> = new Set()
|
|
1225
1249
|
) {
|
|
1226
1250
|
logger.info('GQLtoSQLMapper - processFieldNames', fieldNames, gqlFieldName);
|
|
1227
1251
|
if (fieldNames.length <= 0) {
|
|
@@ -1242,6 +1266,17 @@ export class GQLtoSQLMapper {
|
|
|
1242
1266
|
if (parseJson) {
|
|
1243
1267
|
const jsonExpr = `REPLACE(TRIM(BOTH '"' FROM ${fieldNameWithAlias}::text), '${'\\"'}','"')::jsonb`;
|
|
1244
1268
|
aliasedField = `${jsonExpr} AS "${gqlFieldName}"`;
|
|
1269
|
+
} else if (gqlFieldName in enumFields && enumKeyFields.has(gqlFieldName)) {
|
|
1270
|
+
// CASE WHEN mode: SQL returns the enum string key directly.
|
|
1271
|
+
// Used when the schema is rebuilt from SDL (Apollo Server with
|
|
1272
|
+
// pre-generated schema file), where graphql-js enum values default
|
|
1273
|
+
// to the name string instead of the numeric value.
|
|
1274
|
+
const caseExpr = SQLBuilder.buildEnumCaseSQL(fieldNameWithAlias, enumFields[gqlFieldName]);
|
|
1275
|
+
aliasedField = caseExpr
|
|
1276
|
+
? `${caseExpr} AS "${gqlFieldName}"`
|
|
1277
|
+
: gqlFieldName !== fieldNames[0]
|
|
1278
|
+
? `${fieldNameWithAlias} AS "${gqlFieldName}"`
|
|
1279
|
+
: fieldNameWithAlias;
|
|
1245
1280
|
} else {
|
|
1246
1281
|
aliasedField =
|
|
1247
1282
|
gqlFieldName !== fieldNames[0]
|
|
@@ -128,6 +128,39 @@ export class SQLBuilder {
|
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Builds a SQL CASE expression that maps a raw DB enum column value to its
|
|
133
|
+
* enum string key (name). Used for `mapNumericEnum` fields so the query
|
|
134
|
+
* returns the GraphQL-ready string directly — no post-query conversion.
|
|
135
|
+
*
|
|
136
|
+
* Iterates the enum object's keys. Numeric-valued keys produce a numeric
|
|
137
|
+
* WHEN branch; string-valued keys produce a quoted-string WHEN branch.
|
|
138
|
+
*
|
|
139
|
+
* Example output:
|
|
140
|
+
* CASE "a1"."state"
|
|
141
|
+
* WHEN 0 THEN 'NotStarted'
|
|
142
|
+
* WHEN 1 THEN 'InProgress'
|
|
143
|
+
* ELSE NULL
|
|
144
|
+
* END
|
|
145
|
+
*
|
|
146
|
+
* @param columnExpr Raw SQL column expression (e.g. `"a1"."state"`)
|
|
147
|
+
* @param enumObj The TypeScript enum object (e.g. `QuestState`)
|
|
148
|
+
* @returns CASE WHEN ... THEN ... END expression, or null if no values
|
|
149
|
+
*/
|
|
150
|
+
public static buildEnumCaseSQL(columnExpr: string, enumObj: Record<string, any>): string | null {
|
|
151
|
+
const branches: string[] = [];
|
|
152
|
+
for (const key of Object.keys(enumObj)) {
|
|
153
|
+
// Skip TypeScript's numeric reverse-mapping entries
|
|
154
|
+
if (/^\d+$/.test(key)) continue;
|
|
155
|
+
|
|
156
|
+
const val = enumObj[key];
|
|
157
|
+
const sqlVal = typeof val === 'number' ? val : `'${String(val).replace(/'/g, "''")}'`;
|
|
158
|
+
branches.push(`WHEN ${sqlVal} THEN '${key}'`);
|
|
159
|
+
}
|
|
160
|
+
if (branches.length === 0) return null;
|
|
161
|
+
return `CASE ${columnExpr} ${branches.join(' ')} ELSE NULL END`;
|
|
162
|
+
}
|
|
163
|
+
|
|
131
164
|
public static getFieldMapper =
|
|
132
165
|
<T>(metadata: EntityMetadata<T>, alias: Alias) =>
|
|
133
166
|
(ob: string) => {
|
package/src/types/sql-types.ts
CHANGED
|
@@ -81,6 +81,33 @@ export type FieldBaseSettings = {
|
|
|
81
81
|
* status: { type: () => Status, mapNumericEnum: true, generateFilter: true }
|
|
82
82
|
*/
|
|
83
83
|
mapNumericEnum?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Controls how enum field values are emitted in the SQL query for GraphQL
|
|
86
|
+
* output serialization. Only takes effect when `mapNumericEnum` is also
|
|
87
|
+
* `true`.
|
|
88
|
+
*
|
|
89
|
+
* - `'raw'` (default) — the raw DB value (e.g. `0`, `100`) flows through
|
|
90
|
+
* untouched. Use this when the schema is built **live** via type-graphql's
|
|
91
|
+
* `buildSchema()`, which preserves numeric enum values so graphql-js's
|
|
92
|
+
* `serialize(0)` resolves to `"ACTIVE"` natively.
|
|
93
|
+
* - `'key'` — wraps the column in a `CASE WHEN ... THEN 'Active' END` SQL
|
|
94
|
+
* expression so the query returns the enum string key directly. Use this
|
|
95
|
+
* when the schema is **rebuilt from SDL** (e.g. `printSchema()` →
|
|
96
|
+
* `buildASTSchema()` / Apollo Server with a pre-generated `.graphql` file),
|
|
97
|
+
* because SDL strips numeric values and `serialize(0)` fails.
|
|
98
|
+
*
|
|
99
|
+
* Can also be set globally via `setGlobalConfig({ mapEnumOutput: 'key' })`
|
|
100
|
+
* or the `GQL_OF_POWER_MAP_ENUM_OUTPUT=key` environment variable.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* stateCode: {
|
|
104
|
+
* type: () => StateCode,
|
|
105
|
+
* mapNumericEnum: true,
|
|
106
|
+
* mapEnumOutput: 'key',
|
|
107
|
+
* generateFilter: true,
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
110
|
+
mapEnumOutput?: 'raw' | 'key';
|
|
84
111
|
/**
|
|
85
112
|
* When true, the SQL query wraps this field's column with a JSON parsing
|
|
86
113
|
* expression so the value is returned as a JSON object instead of a raw
|