tslint, check name, nullability, requiredValue -> required

This commit is contained in:
Tim Griesser 2018-11-12 22:19:04 -05:00
parent 2b8b5c0902
commit 1667eb10bf
15 changed files with 179 additions and 54 deletions

View File

@ -19,7 +19,7 @@ const Post = GQLiteralObject("Post", (t) => {
t.field("author", "User", { description: "Author of the field" });
t.string("title", {
description: "Title of the Post",
defaultValue: "Untitled Post",
default: "Untitled Post",
});
t.string("body", { description: "Body of the Post, formatted as Markdown" });
t.field("status", "StatusEnum", {

View File

@ -10,7 +10,7 @@ GQLiteral aims to combine the simplicity and ease of development of schema-first
It builds upon the primitives of `graphql-js` and similar to the schema-first approach, it uses the type names rather than per-type object references to build the schema. What this means is you won't end up with a ton of confusing imports just to build out your types, side-stepping the dreaded circular import problem.
GQLiteral was designed with TypeScript/JavaScript intellisense in mind, and aims to leverage generics and type generation tools to provide as much type coverage as possible to aid in development. Read more about how you can [configure](typescript-setup.md) your project to best take advantage of this.
GQLiteral was designed with TypeScript/JavaScript intellisense in mind, and makes use of TypeScript generics, conditional types, and type merging to provide as much type coverage as possible out of the box. Read more about how you can [configure](typescript-setup.md) your project to best take advantage of this.
## Installation
@ -73,7 +73,7 @@ const schema = GQLiteralSchema({
});
```
## Nullability & defaultValue
## Nullability & default
One benefit of GraphQL is the strict enforcement and guarentees of null values it provides in the type definitions. One opinion held by GraphQL is that fields should be considered nullable by default. The GraphQL documentation provides [this explanation](https://graphql.org/learn/best-practices/#nullability):
@ -87,13 +87,17 @@ If you find yourself wanting this the other way around, there is a `defaultNull`
This can also be configured on a per-type basis, using the `defaultNull` method on the type definition object. This comes in handy where you want to "mix" an `AbstractType` into an `ObjectType` and an `InputObjectType`, and the fields should be considered nullable by default on input, and required by default on output.
#### defaultValue
```
Enforcing non-null guarantees at the resolver layer can be tedious, so GQLiteral also provides a `defaultValue` option; a value used when the resolved type is otherwise `null` or `undefined`. Providing the `defaultValue` will set the schema definition for the field to non-null regardless of root schema configuration, unless `nullable: true` is set explicitly on the field.
```
#### default
Enforcing non-null guarantees at the resolver layer can be tedious, so GQLiteral also provides a `default` option; a value used when the resolved type is otherwise `null` or `undefined`. Providing the `default` will set the schema definition for the field to non-null regardless of root schema configuration, unless `nullable: true` is set explicitly on the field.
```ts
const AccountInfo = GQLiteralObject("AccountInfo", (t) => {
t.string("description", { defaultValue: "N/A" });
t.string("description", { default: "N/A" });
t.int("linkedAccountId", { nullable: true });
});
```

View File

@ -41,7 +41,9 @@ const context = async ({ req }: { req: Request }) => {
const email = new Buffer(auth, "base64").toString("ascii");
// if the email isn't formatted validly, return null for user
if (!isEmail.validate(email)) return { user: null };
if (!isEmail.validate(email)) {
return { user: null };
}
// find a user by their email
const users = await store.users.findOrCreate({ where: { email } });
const user = users && users[0] ? users[0] : null;

View File

@ -6,6 +6,6 @@ export const Droid = GQLiteralObject<Gen, "Droid">("Droid", (t) => {
t.implements("Character");
t.string("primaryFunction", {
description: "The primary function of the droid.",
defaultValue: "N/A",
default: "N/A",
});
});

View File

@ -2,12 +2,16 @@
"name": "gqliteral",
"version": "0.1.0",
"main": "dist/index.js",
"types": "src/index.ts",
"types": "dist/index.d.ts",
"license": "MIT",
"description": "Scalable, strongly typed GraphQL schema development",
"scripts": {
"dev": "tsc -w",
"test": "jest"
"test": "jest",
"build": "tsc",
"lint": "tslint -p tsconfig.json",
"clean": "rm -rf dist",
"prepublish": "yarn clean && yarn lint && yarn build"
},
"files": [
"src",
@ -34,8 +38,10 @@
"lint-staged": "^7.3.0",
"typescript": "^3.1.3",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
"jest": "^23.6.0",
"ts-jest": "^23.10.4"
"ts-jest": "^23.10.4",
"tslib": "^1.9.3"
},
"peerDependencies": {
"prettier": "^1.15.1",

View File

@ -36,7 +36,7 @@ import {
} from "graphql";
import { GQLiteralTypeWrapper } from "./definitions";
import * as Types from "./types";
import suggestionList, { propertyFieldResolver } from "./utils";
import { suggestionList, propertyFieldResolver } from "./utils";
import { GQLiteralAbstract, GQLiteralDirectiveType } from "./objects";
const isPromise = (val: any): val is Promise<any> =>
@ -172,8 +172,8 @@ export class SchemaBuilder {
...rest,
args: args.reduce(
(result: GraphQLFieldConfigArgumentMap, arg) => {
const { name, ...rest } = arg;
result[name] = rest;
const { name, ...argRest } = arg;
result[name] = argRest;
return result;
},
{}
@ -446,10 +446,20 @@ export class SchemaBuilder {
protected decorateInputType(
type: GraphQLInputType,
fieldConfig: Types.FieldConfig,
fieldConfig: Types.InputFieldConfig,
typeConfig: Types.InputTypeConfig
) {
return this.decorateType(type, fieldConfig, typeConfig, true);
const { required: _required, requiredListItem, ...rest } = fieldConfig;
const newOpts = rest;
if (typeof _required !== "undefined") {
newOpts.nullable = !_required;
}
if (typeof requiredListItem !== "undefined") {
if (rest.list) {
newOpts.listItemNullable = !requiredListItem;
}
}
return this.decorateType(type, newOpts, typeConfig, true);
}
protected decorateOutputType(
@ -640,8 +650,8 @@ export class SchemaBuilder {
} else if (fieldOptions.property) {
resolver = propertyFieldResolver(fieldOptions.property);
}
if (typeof fieldOptions.defaultValue !== "undefined") {
resolver = withDefaultValue(resolver, fieldOptions.defaultValue);
if (typeof fieldOptions.default !== "undefined") {
resolver = withDefaultValue(resolver, fieldOptions.default);
}
return resolver;
}

View File

@ -6,6 +6,7 @@ import {
printSchema,
GraphQLScalarType,
GraphQLObjectType,
assertValidName,
} from "graphql";
import * as Types from "./types";
import {
@ -45,7 +46,7 @@ export class GQLiteralTypeWrapper<
* @param {object} options
*/
export function GQLiteralScalar(name: string, options: Types.ScalarOpts) {
return new GraphQLScalarType({ name, ...options });
return new GraphQLScalarType({ name: assertValidName(name), ...options });
}
/**
@ -57,7 +58,9 @@ export function GQLiteralObject<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(name: TypeName, fn: (arg: GQLiteralObjectType<GenTypes, TypeName>) => void) {
const factory = new GQLiteralObjectType<GenTypes, TypeName>(name);
const factory = new GQLiteralObjectType<GenTypes, TypeName>(
assertValidName(name)
);
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -72,7 +75,9 @@ export function GQLiteralInterface<
name: TypeName,
fn: (arg: GQLiteralInterfaceType<GenTypes, TypeName>) => void
) {
const factory = new GQLiteralInterfaceType<GenTypes, TypeName>(name);
const factory = new GQLiteralInterfaceType<GenTypes, TypeName>(
assertValidName(name)
);
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -98,7 +103,7 @@ export function GQLiteralUnion<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(name: TypeName, fn: (arg: GQLiteralUnionType<GenTypes, TypeName>) => void) {
const factory = new GQLiteralUnionType<GenTypes>(name);
const factory = new GQLiteralUnionType<GenTypes>(assertValidName(name));
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -140,7 +145,7 @@ export function GQLiteralEnum<
| string[]
| Record<string, string | number | object | boolean>
) {
const factory = new GQLiteralEnumType<GenTypes>(name);
const factory = new GQLiteralEnumType<GenTypes>(assertValidName(name));
if (typeof fn === "function") {
fn(factory);
} else {
@ -156,7 +161,7 @@ export function GQLiteralInputObject<
GenTypes = GQLiteralGen,
TypeName extends string = any
>(name: TypeName, fn: (arg: GQLiteralInputObjectType<GenTypes>) => void) {
const factory = new GQLiteralInputObjectType<GenTypes>(name);
const factory = new GQLiteralInputObjectType<GenTypes>(assertValidName(name));
fn(factory);
return new GQLiteralTypeWrapper(name, factory);
}
@ -213,7 +218,7 @@ export function GQLiteralDirective<
| Types.DirectiveConfig<GenTypes, DirectiveName>
| ((arg: GQLiteralDirectiveType<GenTypes>) => void)
) {
const directive = new GQLiteralDirectiveType<GenTypes>(name);
const directive = new GQLiteralDirectiveType<GenTypes>(assertValidName(name));
if (typeof config === "function") {
config(directive);
} else {
@ -232,14 +237,14 @@ export function GQLiteralDirective<
export function GQLiteralSchema(options: Types.SchemaConfig) {
const { types: typeMap, directives } = buildTypes(options.types, options);
if (!isObjectType(typeMap["Query"])) {
if (!isObjectType(typeMap.Query)) {
throw new Error("Missing a Query type");
}
const schema = new GraphQLSchema({
query: typeMap["Query"] as Types.Maybe<GraphQLObjectType>,
mutation: typeMap["Mutation"] as Types.Maybe<GraphQLObjectType>,
subscription: typeMap["Subscription"] as Types.Maybe<GraphQLObjectType>,
query: typeMap.Query as Types.Maybe<GraphQLObjectType>,
mutation: typeMap.Mutation as Types.Maybe<GraphQLObjectType>,
subscription: typeMap.Subscription as Types.Maybe<GraphQLObjectType>,
directives: directives.definitions,
types: Object.keys(typeMap).reduce((result: GraphQLNamedType[], key) => {
result.push(typeMap[key]);
@ -250,7 +255,10 @@ export function GQLiteralSchema(options: Types.SchemaConfig) {
// Only in development do we want to worry about regenerating the
// schema definition and/or generated types.
if (process.env.NODE_ENV !== "production") {
const sortedSchema = lexicographicSortSchema(schema);
let sortedSchema = schema;
if (typeof lexicographicSortSchema !== "undefined") {
sortedSchema = lexicographicSortSchema(schema);
}
const generatedSchema = addDirectives(
printSchema(sortedSchema),
directives

View File

@ -10,3 +10,5 @@ export {
GQLiteralAbstractType,
GQLiteralDirective,
} from "./definitions";
import * as GQLiteralTypes from "./types";
export { GQLiteralTypes };

View File

@ -38,7 +38,7 @@ export class GQLiteralEnumType<GenTypes = GQLiteralGen> {
};
}
mix<EnumName extends Types.EnumName<GenTypes>>(
mix<EnumName extends Types.EnumNames<GenTypes>>(
typeName: EnumName,
mixOptions?: Types.MixOpts<Types.EnumMembers<GenTypes, EnumName>>
) {
@ -64,16 +64,16 @@ export class GQLiteralEnumType<GenTypes = GQLiteralGen> {
* Sets the members of the enum
*/
members(info: Array<Types.EnumMemberInfo | string>) {
info.forEach((info) => {
if (typeof info === "string") {
info.forEach((member) => {
if (typeof member === "string") {
return this.typeConfig.members.push({
item: Types.NodeType.ENUM_MEMBER,
info: { name: info, value: info },
info: { name: member, value: member },
});
}
this.typeConfig.members.push({
item: Types.NodeType.ENUM_MEMBER,
info,
info: member,
});
});
}
@ -333,7 +333,7 @@ export class GQLiteralObjectType<
* abstract type.
*
* At this point the type will not change, but the resolver,
* defaultValue, property, or description fields can.
* default, property, or description fields can.
*/
modify<FieldName extends Types.ObjectTypeFields<GenTypes, TypeName>>(
field: FieldName,
@ -365,6 +365,20 @@ export class GQLiteralObjectType<
});
}
/**
* Set the nullability config for the type
*/
nullability(nullability: Types.NullabilityConfig) {
if (this.typeConfig.nullability) {
console.warn(
`nullability has already been set for type ${
this.typeConfig.name
}, the previous value will be replaced`
);
}
this.typeConfig.nullability = nullability;
}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.
@ -600,6 +614,20 @@ export class GQLiteralInputObjectType<GenTypes = GQLiteralGen> {
});
}
/**
* Set the nullability config for the type
*/
nullability(nullability: Types.NullabilityConfig) {
if (this.typeConfig.nullability) {
console.warn(
`nullability has already been set for type ${
this.typeConfig.name
}, the previous value will be replaced`
);
}
this.typeConfig.nullability = nullability;
}
/**
* Internal use only. Creates the configuration to create
* the GraphQL named type.
@ -702,7 +730,7 @@ export class GQLiteralDirectiveType<GenTypes = GQLiteralGen> {
this.typeConfig = {
name,
locations: [],
args: [],
directiveArgs: [],
};
}
@ -772,7 +800,7 @@ export class GQLiteralDirectiveType<GenTypes = GQLiteralGen> {
type: Types.AllInputTypes<GenTypes> | Types.BaseScalars,
options?: Types.InputFieldOpts
) {
this.typeConfig.args.push({
this.typeConfig.directiveArgs.push({
name,
type,
...options,

View File

@ -290,14 +290,14 @@ function nonInterfaceFields(
ctx: SchemaTemplateContext,
t: SchemaTemplateContext["types"][0]
) {
const interfaceFields = new Set<string>();
const allInterfaceFields = new Set<string>();
t.interfaces.forEach((name) => {
const iface = ctx.interfaces.find((i) => i.name === name);
if (iface) {
iface.fields.forEach((field) => {
interfaceFields.add(field.name);
allInterfaceFields.add(field.name);
});
}
});
return t.fields.filter((field) => !interfaceFields.has(field.name));
return t.fields.filter((field) => !allInterfaceFields.has(field.name));
}

View File

@ -48,6 +48,11 @@ export interface FieldConfig extends OutputFieldOpts<any, any, any> {
type: any;
}
export interface InputFieldConfig extends InputFieldOpts {
name: string;
type: any;
}
export type FieldDefType = MixDef | MixAbstractDef | FieldDef;
export type EnumDefType =
@ -204,7 +209,12 @@ export interface OutputFieldOpts<
/**
* Default value for the field, if none is returned.
*/
defaultValue?: any;
default?: any;
/**
* Synchronous validation for the field, runs just after the
* built-in "validate", at the root level prior to execution.
*/
validate?: (info: GraphQLResolveInfo) => boolean | Error;
}
export interface AbstractFieldOpts<GenTypes, FieldName> extends FieldOpts {}
@ -214,7 +224,16 @@ export type ModifyFieldOpts<GenTypes, TypeName, FieldName> = Omit<
"args" | "list" | "listItemNullable" | "nullable"
>;
export interface InputFieldOpts extends FieldOpts {}
export interface InputFieldOpts extends FieldOpts {
/**
* Setting this to true is the same as setting `nullable: false`
*/
required?: boolean;
/**
* Whether the item in the list is required
*/
requiredListItem?: boolean;
}
export interface ScalarOpts
extends Omit<
@ -314,7 +333,7 @@ export interface AbstractTypeConfig {
export interface DirectiveTypeConfig extends Named {
description?: string;
locations: DirectiveLocationEnum[];
args: DirectiveArgDefinition[];
directiveArgs: DirectiveArgDefinition[];
}
export interface InterfaceTypeConfig
@ -420,7 +439,7 @@ export type TypeResolver<GenTypes, TypeName> = (
root: RootValue<GenTypes, TypeName>,
context: ContextValue<GenTypes>,
info: GraphQLResolveInfo
) => MaybePromise<Maybe<InterfaceName<GenTypes, TypeName>>>;
) => MaybePromise<Maybe<InterfaceNames<GenTypes, TypeName>>>;
/**
* Helpers for handling the generated schema
@ -441,17 +460,17 @@ export type OutputNames<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypes["objects"], string>
: never;
export type InterfaceName<GenTypes, TypeName> = GenTypes extends GenTypesShape
export type InterfaceNames<GenTypes, TypeName> = GenTypes extends GenTypesShape
? TypeName extends keyof GenTypes["interfaces"]
? GenTypes["interfaces"][TypeName]["implementingTypes"]
: never
: never;
export type EnumName<GenTypes> = GenTypes extends GenTypesShape
export type EnumNames<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypes["enums"], string>
: never;
export type UnionName<GenTypes> = GenTypes extends GenTypesShape
export type UnionNames<GenTypes> = GenTypes extends GenTypesShape
? Extract<keyof GenTypes["unions"], string>
: never;

View File

@ -134,10 +134,7 @@ export function addDirectives(
* Given an invalid input string and a list of valid options, returns a filtered
* list of valid options sorted based on their similarity with the input.
*/
export default function suggestionList(
input: string,
options: string[]
): string[] {
export function suggestionList(input: string, options: string[]): string[] {
var optionsByDistance = Object.create(null);
var oLength = options.length;
var inputThreshold = input.length / 2;

View File

@ -8,7 +8,9 @@
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true
"skipLibCheck": true,
"declaration": true,
"importHelpers": true
},
"exclude": ["./examples", "./dist", "./src/__tests__"]
}

43
tslint.json Normal file
View File

@ -0,0 +1,43 @@
{
"extends": ["tslint-config-prettier"],
"rules": {
"ban": false,
"class-name": true,
"curly": true,
"eofline": false,
"forin": true,
"indent": [true, "spaces"],
"jsdoc-format": true,
"label-position": true,
"no-any": false,
"no-arg": true,
"no-bitwise": true,
"no-consecutive-blank-lines": false,
"no-construct": true,
"no-debugger": false,
"no-duplicate-variable": true,
"no-empty": false,
"no-eval": true,
"no-default-export": true,
"no-shadowed-variable": true,
"no-string-literal": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": false,
"no-unused-expression": true,
"one-line": [
true,
"check-catch",
"check-else",
"check-open-brace",
"check-whitespace"
],
"radix": true,
"restrict-plus-operands": true,
"trailing-comma": [false],
"triple-equals": [true, "allow-null-check"],
"whitespace": false
},
"linterOptions": {
"exclude": ["**/*.json"]
}
}

View File

@ -3611,10 +3611,14 @@ ts-log@2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.1.3.tgz#9e30aca1baffe7693a2e4142b8f07ecb01cb8340"
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
tslint-config-prettier@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.15.0.tgz#76b9714399004ab6831fdcf76d89b73691c812cf"
tslint@^5.11.0:
version "5.11.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed"