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:
parent
8aa96979ce
commit
459f3cfff1
|
|
@ -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?: (
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue