Allow enum backing types (#142)

* Allow enums to have backing types

* Update typeMatch regex references

* Ignore const enums for backing-types.

* Allow native TS enum as members config

* Allow const enum backing types

* Add assertValidName for enum keys
This commit is contained in:
Rutger Hendrickx 2019-06-05 14:55:07 +02:00 committed by Tim Griesser
parent 8aa96979ce
commit 459f3cfff1
8 changed files with 224 additions and 27 deletions

View File

@ -88,7 +88,9 @@ export interface TypegenConfigSourceModule {
*
* If not provided, the default implementation is:
*
* (type) => new RegExp('(?:interface|type|class)\s+(${type.name})\W')
* (type) => [
* new RegExp(`(?:interface|type|class|enum)\\s+(${type.name})\\W`, "g")
* ]
*
*/
typeMatch?: (

View File

@ -33,6 +33,7 @@ import {
isUnionType,
isScalarType,
defaultFieldResolver,
assertValidName,
getNamedType,
GraphQLField,
} from "graphql";
@ -633,11 +634,20 @@ export class SchemaBuilder {
}
});
} else {
Object.keys(members).forEach((key) => {
values[key] = {
value: members[key],
};
});
Object.keys(members)
// members can potentially be a TypeScript enum.
// The compiled version of this enum will be the members object,
// numeric enums members also get a reverse mapping from enum values to enum names.
// In these cases we have to ensure we don't include these reverse mapping keys.
// See: https://www.typescriptlang.org/docs/handbook/enums.html
.filter((key) => isNaN(+key))
.forEach((key) => {
assertValidName(key);
values[key] = {
value: (members as Record<string, string | number | symbol>)[key],
};
});
}
if (!Object.keys(values).length) {
throw new Error(

View File

@ -1,6 +1,10 @@
import { NexusTypes, withNexusSymbol, RootTypingDef } from "./_types";
import { assertValidName } from "graphql";
type TypeScriptEnumLike = {
[key: number]: string;
};
export interface EnumMemberInfo {
/**
* The external "value" of the enum as displayed in the SDL
@ -32,11 +36,12 @@ export interface EnumTypeConfig<TypeName extends string> {
*/
rootTyping?: RootTypingDef;
/**
* All members of the enum, either as an array of strings/definition objects, or as an object
* All members of the enum, either as an array of strings/definition objects, as an object, or as a TypeScript enum
*/
members:
| Array<string | EnumMemberInfo>
| Record<string, string | number | object | boolean>;
| Record<string, string | number | object | boolean>
| TypeScriptEnumLike;
}
export class NexusEnumTypeDef<TypeName extends string> {

View File

@ -302,8 +302,13 @@ export class Typegen {
buildEnumTypeMap() {
const enumMap: TypeMapping = {};
this.groupedTypes.enum.forEach((e) => {
const values = e.getValues().map((val) => JSON.stringify(val.value));
enumMap[e.name] = values.join(" | ");
const backingType = this.typegenInfo.backingTypeMap[e.name];
if (backingType) {
enumMap[e.name] = backingType;
} else {
const values = e.getValues().map((val) => JSON.stringify(val.value));
enumMap[e.name] = values.join(" | ");
}
});
return enumMap;
}

View File

@ -1,9 +1,4 @@
import {
GraphQLSchema,
isOutputType,
GraphQLNamedType,
isEnumType,
} from "graphql";
import { GraphQLSchema, isOutputType, GraphQLNamedType } from "graphql";
import path from "path";
import { TYPEGEN_HEADER } from "./lang";
import { log, objValues, relativePathTo } from "./utils";
@ -38,9 +33,11 @@ export interface TypegenConfigSourceModule {
* Provides a custom approach to matching for the type
*
* If not provided, the default implementation is:
* ```
* (type) => new RegExp('(?:interface|type|class)\s+(${type.name})\W')
* ```
*
* (type) => [
* new RegExp(`(?:interface|type|class|enum)\\s+(${type.name})\\W`, "g"),
* ]
*
*/
typeMatch?: (
type: GraphQLNamedType,
@ -260,8 +257,8 @@ export function typegenAutoConfig(options: TypegenAutoConfigOptions) {
const type = schema.getType(typeName);
// For now we'll say that if it's non-enum output type it can be backed
if (isOutputType(type) && !isEnumType(type)) {
// For now we'll say that if it's output type it can be backed
if (isOutputType(type)) {
for (let i = 0; i < typeSources.length; i++) {
const typeSource = typeSources[i];
if (!typeSource) {
@ -376,5 +373,7 @@ const firstMatch = (
};
const defaultTypeMatcher = (type: GraphQLNamedType) => {
return [new RegExp(`(?:interface|type|class)\\s+(${type.name})\\W`, "g")];
return [
new RegExp(`(?:interface|type|class|enum)\\s+(${type.name})\\W`, "g"),
];
};

View File

@ -8,3 +8,13 @@ export interface User {
email: string;
fullName(): string;
}
export enum A {
ONE = "ONE",
TWO = "TWO",
}
export const enum B {
NINE = "9",
TEN = "10",
}

103
tests/backingTypes.spec.ts Normal file
View File

@ -0,0 +1,103 @@
import path from "path";
import { core, makeSchema, queryType, enumType } from "../src";
import { A, B } from "./_types";
import { NexusSchemaExtensions } from "../src/core";
const { Typegen, TypegenMetadata } = core;
function getSchemaWithNormalEnums() {
return makeSchema({
types: [
enumType({
name: "A",
members: [A.ONE, A.TWO],
}),
queryType({
definition(t) {
t.field("a", { type: "A" });
},
}),
],
outputs: false,
});
}
function getSchemaWithConstEnums() {
return makeSchema({
types: [
enumType({
name: "B",
members: [B.NINE, B.TEN],
}),
queryType({
definition(t) {
t.field("b", { type: "B" });
},
}),
],
outputs: false,
});
}
describe("backingTypes", () => {
let metadata: core.TypegenMetadata;
let schemaExtensions: NexusSchemaExtensions;
beforeEach(async () => {
metadata = new TypegenMetadata({
outputs: {
typegen: path.join(__dirname, "test-gen.ts"),
schema: path.join(__dirname, "test-gen.graphql"),
},
typegenAutoConfig: {
sources: [
{
alias: "t",
source: path.join(__dirname, "_types.ts"),
},
],
contextType: "t.TestContext",
},
});
});
schemaExtensions = {
rootTypings: {},
dynamicFields: {
dynamicInputFields: {},
dynamicOutputFields: {},
},
};
it("can match backing types to regular enums", async () => {
const schema = getSchemaWithNormalEnums();
const typegenInfo = await metadata.getTypegenInfo(schema);
const typegen = new Typegen(
schema,
{ ...typegenInfo, typegenFile: "" },
schemaExtensions
);
expect(typegen.printEnumTypeMap()).toMatchInlineSnapshot(`
"export interface NexusGenEnums {
A: t.A
}"
`);
});
it("can match backing types for const enums", async () => {
const schema = getSchemaWithConstEnums();
const typegenInfo = await metadata.getTypegenInfo(schema);
const typegen = new Typegen(
schema,
{ ...typegenInfo, typegenFile: "" },
schemaExtensions
);
expect(typegen.printEnumTypeMap()).toMatchInlineSnapshot(`
"export interface NexusGenEnums {
B: t.B
}"
`);
});
});

View File

@ -11,13 +11,24 @@ import {
} from "../src";
import { UserObject, PostObject } from "./_helpers";
describe("enumType", () => {
const PrimaryColors = enumType({
name: "PrimaryColors",
members: ["RED", "YELLOW", "BLUE"],
});
enum NativeColors {
RED = "RED",
BLUE = "BLUE",
GREEN = "green", // lower case to ensure we grab correct keys
}
enum NativeNumbers {
ONE = 1,
TWO = 2,
THREE = 3,
}
describe("enumType", () => {
it("builds an enum", () => {
const PrimaryColors = enumType({
name: "PrimaryColors",
members: ["RED", "YELLOW", "BLUE"],
});
const types = buildTypes<{ PrimaryColors: GraphQLEnumType }>([
PrimaryColors,
]);
@ -27,6 +38,58 @@ describe("enumType", () => {
);
});
it("builds an enum from a TypeScript enum with string values", () => {
const Colors = enumType({
name: "Colors",
members: NativeColors,
});
const types = buildTypes<{ Colors: GraphQLEnumType }>([Colors]);
expect(types.typeMap.Colors).toBeInstanceOf(GraphQLEnumType);
expect(types.typeMap.Colors.getValues()).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "RED",
value: "RED",
}),
expect.objectContaining({
name: "BLUE",
value: "BLUE",
}),
expect.objectContaining({
name: "GREEN",
value: "green",
}),
])
);
});
it("builds an enum from a TypeScript enum with number values", () => {
const Numbers = enumType({
name: "Numbers",
members: NativeNumbers,
});
const types = buildTypes<{ Numbers: GraphQLEnumType }>([Numbers]);
expect(types.typeMap.Numbers).toBeInstanceOf(GraphQLEnumType);
expect(types.typeMap.Numbers.getValues()).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "ONE",
value: 1,
}),
expect.objectContaining({
name: "TWO",
value: 2,
}),
expect.objectContaining({
name: "THREE",
value: 3,
}),
])
);
});
it("can map internal values", () => {
const Internal = enumType({
name: "Internal",