Skip to content

Support extensions in schema #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
# except these
!lib/**/*
!src/**/*
!package-lock.json
!package.json
!README.md
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,26 @@
"devDependencies": {
"@playlyfe/gql": "2.3.2",
"@types/fs-extra": "4.0.4",
"@types/graphql": "0.11.5",
"@types/graphql": "^14.5.0",
"@types/jest": "21.1.5",
"@types/node": "8.0.50",
"@types/lodash": "^4.14.149",
"@types/node": "^13.9.2",
"@types/yargs": "^11.0.0",
"del-cli": "^2.0.0",
"fs-extra": "4.0.2",
"graphql": "0.11.7",
"graphql-tools": "2.7.2",
"graphql": "^14.6.0",
"jest": "22.4.3",
"lodash": "^4.17.13",
"ts-jest": "22.4.4",
"tslint": "5.8.0",
"typescript": "2.9.2"
"typescript": "^3.8.3"
},
"dependencies": {
"graphql-tools": "^4.0.7",
"lodash": "^4.17.15",
"yargs": "^11.0.0"
},
"peerDependencies": {
"graphql": "*",
"graphql": ">= 0.12",
"typescript": "*"
},
"jest": {
Expand Down
49 changes: 49 additions & 0 deletions src/__tests__/__snapshots__/schemaExtendDeclarations.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`schema extension declaration should generate if schema is a valid graphql schema 1`] = `
"/* tslint:disable */
/* eslint-disable */
import { GraphQLResolveInfo } from 'graphql';
/**
* This file is auto-generated by @raraujo/graphql-schema-typescript
* Please note that any changes in this file may be overwritten
*/


/*******************************
* *
* TYPE DEFS *
* *
*******************************/
export interface GQLQuery {
test: string;
foo?: string;
}

/*********************************
* *
* TYPE RESOLVERS *
* *
*********************************/
/**
* This interface define the shape of your resolver
* Note that this type is designed to be compatible with graphql-tools resolvers
* However, you can still use other generated interfaces to make your resolver type-safed
*/
export interface GQLResolver {
Query?: GQLQueryTypeResolver;
}
export interface GQLQueryTypeResolver<TParent = any> {
test?: QueryToTestResolver<TParent>;
foo?: QueryToFooResolver<TParent>;
}

export interface QueryToTestResolver<TParent = any, TResult = any> {
(parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult;
}

export interface QueryToFooResolver<TParent = any, TResult = any> {
(parent: TParent, args: {}, context: any, info: GraphQLResolveInfo): TResult;
}
"
`;
12 changes: 12 additions & 0 deletions src/__tests__/schemaExtendDeclarations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as path from 'path';
import { generateTypeScriptTypes } from '..';
import { executeApiTest } from './testUtils';

describe('schema extension declaration', () => {

it('should generate if schema is a valid graphql schema', async () => {
const schemaStr = `schema { query: Query } type Query { test: String! } extend type Query { foo: String }`;
const generated = await executeApiTest('schemaString.ts', {}, schemaStr);
expect(generated).toEqual(expect.stringContaining('foo?: string'));
});
});
5 changes: 3 additions & 2 deletions src/__tests__/testSchema/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as fs from 'fs';
import * as path from 'path';
import { makeExecutableSchema } from 'graphql-tools';
import { GraphQLSchema, introspectionQuery, graphql } from 'graphql';
import { GraphQLSchema } from 'graphql';

const gqlFiles = fs.readdirSync(__dirname).filter(f => f.endsWith('.gql') || f.endsWith('.graphql'));

const typeDefs = gqlFiles.map(filePath => fs.readFileSync(path.join(__dirname, filePath), 'utf-8'));

export const testSchema: GraphQLSchema = makeExecutableSchema({
typeDefs: typeDefs
typeDefs: typeDefs,
resolverValidationOptions: {requireResolversForResolveType: false}
});
14 changes: 7 additions & 7 deletions src/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const executeCommand = (command: string, options?: childProcess.ExecOptio
return new Promise((resolve, reject) => {
const process = childProcess.exec(command, options);

process.stdout.on('data', console.log);
process.stderr.on('data', console.error);
process.stdout!.on('data', console.log);
process.stderr!.on('data', console.error);

process.on('close', (exitCode: number) => {
if (exitCode !== 0) {
Expand All @@ -28,7 +28,7 @@ export const executeCommand = (command: string, options?: childProcess.ExecOptio
};

/** Function that count occurrences of a substring in a string;
* @param {String} string The string
* @param {String} str The string
* @param {String} subString The sub string to search for
* @param {Boolean} [allowOverlapping] Optional. (Default:false)
*
Expand All @@ -44,7 +44,7 @@ export const occurrences = (str: string, subString: string, allowOverlapping: bo
return (str.length + 1);
}

var n = 0,
let n = 0,
pos = 0,
step = allowOverlapping ? 1 : subString.length;

Expand All @@ -63,7 +63,7 @@ export const occurrences = (str: string, subString: string, allowOverlapping: bo
export const OUTPUT_FOLDER = path.join(__dirname, 'generatedTypes');
export const executeApiTest = async (
outputFile: string,
options: GenerateTypescriptOptions,
options: Partial<GenerateTypescriptOptions>,
schema?: GraphQLSchema | string
): Promise<string> => {
// prepare output folder
Expand All @@ -73,11 +73,11 @@ export const executeApiTest = async (
const outputPath = path.join(OUTPUT_FOLDER, outputFile);

// run api function
await generateTypeScriptTypes(schema || testSchema, outputPath, options);
generateTypeScriptTypes(schema || testSchema, outputPath, options as GenerateTypescriptOptions);

// ensure no error on tsc
const generated = fs.readFileSync(outputPath, 'utf-8');
await executeCommand(`tsc --noEmit --lib es6,esnext.asynciterable --target es5 ${outputPath}`);
await executeCommand(`yarn -s tsc --noEmit --lib es6,esnext.asynciterable --target es5 ${outputPath}`);

// snapshot
expect(generated).toMatchSnapshot();
Expand Down
7 changes: 0 additions & 7 deletions src/apollo-link.d.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ yargs
async argv => {
const { folderPath, output } = argv;

const options: GenerateTypescriptOptions = {};
const options: Partial<GenerateTypescriptOptions> = {};
options[globalOpt] = argv[globalOpt];
options[typePrefix] = argv[typePrefix];
options[namespaceOpt] = argv[namespaceOpt];
Expand All @@ -116,7 +116,7 @@ yargs
options[noStringEnum] = argv[noStringEnum];
options[optionalResolverInfo] = argv[optionalResolverInfo];

await generateTypeScriptTypes(folderPath, path.resolve(output), options);
generateTypeScriptTypes(folderPath, path.resolve(output), options as GenerateTypescriptOptions);
if (process.env.NODE_ENV !== 'test') {
console.log(`Typescript generated at: ${output}`);
}
Expand Down
32 changes: 13 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as fs from 'fs';
import * as path from 'path';
import { GraphQLSchema, buildSchema } from 'graphql';
import { GraphQLSchema } from 'graphql';
import { GenerateTypescriptOptions, defaultOptions } from './types';
import { TSResolverGenerator, GenerateResolversResult } from './typescriptResolverGenerator';
import { TypeScriptGenerator } from './typescriptGenerator';
import { formatTabSpace, introspectSchema, introspectSchemaViaLocalFile } from './utils';
import { isString } from 'util';
import { formatTabSpace, introspectSchema, introspectSchemaStr, introspectSchemaViaLocalFile } from './utils';
import { IntrospectionQuery } from 'graphql/utilities/introspectionQuery';

export { GenerateTypescriptOptions } from './types';
Expand Down Expand Up @@ -36,21 +35,21 @@ const typeResolversDecoration = [
' *********************************/'
];

export const generateTSTypesAsString = async (
export const generateTSTypesAsString = (
schema: GraphQLSchema | string,
outputPath: string,
options: GenerateTypescriptOptions
): Promise<string> => {
): string => {
const mergedOptions = { ...defaultOptions, ...options };

let introspectResult: IntrospectionQuery;
if (isString(schema)) {
let introspectResult: IntrospectionQuery | null = null;
if (typeof schema === 'string') {
// is is a path to schema folder?
try {
const schemaPath = path.resolve(schema);
const exists = fs.existsSync(schemaPath);
if (exists) {
introspectResult = await introspectSchemaViaLocalFile(schemaPath);
introspectResult = introspectSchemaViaLocalFile(schemaPath);
}
} catch {
// fall-through in case the provided string is a graphql definition,
Expand All @@ -59,22 +58,17 @@ export const generateTSTypesAsString = async (

// it's not a folder, maybe it's a schema definition
if (!introspectResult) {
const schemaViaStr = buildSchema(schema);
introspectResult = await introspectSchema(schemaViaStr);
introspectResult = introspectSchemaStr(schema);
}
} else {
introspectResult = await introspectSchema(schema);
introspectResult = introspectSchema(schema);
}

const tsGenerator = new TypeScriptGenerator(mergedOptions, introspectResult, outputPath);
const typeDefs = await tsGenerator.generate();
const typeDefs = tsGenerator.generate();

let typeResolvers: GenerateResolversResult = {
body: [],
importHeader: []
};
const tsResolverGenerator = new TSResolverGenerator(mergedOptions, introspectResult);
typeResolvers = await tsResolverGenerator.generate();
const typeResolvers = tsResolverGenerator.generate();

let header = [...typeResolvers.importHeader, jsDoc];

Expand Down Expand Up @@ -108,11 +102,11 @@ export const generateTSTypesAsString = async (
return formatted.join('\n');
};

export async function generateTypeScriptTypes(
export function generateTypeScriptTypes(
schema: GraphQLSchema | string,
outputPath: string,
options: GenerateTypescriptOptions = defaultOptions
) {
const content = await generateTSTypesAsString(schema, outputPath, options);
const content = generateTSTypesAsString(schema, outputPath, options);
fs.writeFileSync(outputPath, content, 'utf-8');
}
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export interface GenerateTypescriptOptions {
};

/** Tab format, default to 2 */
tabSpaces?: number;
tabSpaces: number;

/** A prefix to every generated types. Default to GQL */
typePrefix?: string;
typePrefix: string;

/** Generate types as global */
global?: boolean;
Expand Down
20 changes: 9 additions & 11 deletions src/typescriptGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ import {
IntrospectionInputObjectType,
IntrospectionInterfaceType,
IntrospectionQuery,
IntrospectionField,
IntrospectionInputValue
} from 'graphql';
import { IntrospectionField, IntrospectionInputValue } from 'graphql/utilities/introspectionQuery';

export class TypeScriptGenerator {

Expand All @@ -27,15 +26,15 @@ export class TypeScriptGenerator {
protected outputPath: string
) { }

public async generate(): Promise<string[]> {
public generate(): string[] {
const { introspectResult } = this;
const gqlTypes = introspectResult.__schema.types.filter(type => !isBuiltinType(type));

return gqlTypes.reduce<string[]>(
(prevTypescriptDefs, gqlType) => {

const jsDoc = descriptionToJSDoc({ description: gqlType.description });
let typeScriptDefs: string[] = [].concat(jsDoc);
let typeScriptDefs = jsDoc.slice(0);

switch (gqlType.kind) {
case 'SCALAR': {
Expand Down Expand Up @@ -138,15 +137,15 @@ export class TypeScriptGenerator {
if (this.options.enumsAsPascalCase) {
return pascalCase(graphQlName);
} else {
return graphQlName;
return graphQlName;
}
}

private generateObjectType(
objectType: IntrospectionObjectType | IntrospectionInputObjectType | IntrospectionInterfaceType,
allGQLTypes: IntrospectionType[]
): string[] {
const fields: (IntrospectionInputValue | IntrospectionField)[]
const fields: ReadonlyArray<IntrospectionInputValue | IntrospectionField>
= objectType.kind === 'INPUT_OBJECT' ? objectType.inputFields : objectType.fields;

const extendTypes: string[] = objectType.kind === 'OBJECT'
Expand All @@ -161,15 +160,14 @@ export class TypeScriptGenerator {
[]
);

const objectFields = fields.reduce<string[]>(
(prevTypescriptDefs, field, index) => {

const objectFields = fields.reduce(
(prevTypescriptDefs, field) => {
if (extendFields.indexOf(field.name) !== -1 && this.options.minimizeInterfaceImplementation) {
return prevTypescriptDefs;
}

const fieldJsDoc = descriptionToJSDoc(field);
const { fieldName, fieldType } = createFieldRef(field, this.options.typePrefix, this.options.strictNulls);
const { fieldName, fieldType } = createFieldRef(field, this.options.typePrefix, !!this.options.strictNulls);
const fieldNameAndType = `${fieldName}: ${fieldType};`;
let typescriptDefs = [...fieldJsDoc, fieldNameAndType];

Expand All @@ -180,7 +178,7 @@ export class TypeScriptGenerator {
return prevTypescriptDefs.concat(typescriptDefs);

},
[]
[] as string[]
);

const possibleTypeNames: string[] = [];
Expand Down
8 changes: 4 additions & 4 deletions src/typescriptResolverGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export class TSResolverGenerator {
} = {};
protected contextType: string;

protected queryType?: IntrospectionNamedTypeRef;
protected mutationType?: IntrospectionNamedTypeRef;
protected subscriptionType?: IntrospectionNamedTypeRef;
protected queryType?: IntrospectionNamedTypeRef<IntrospectionObjectType> | null;
protected mutationType?: IntrospectionNamedTypeRef | null;
protected subscriptionType?: IntrospectionNamedTypeRef<IntrospectionObjectType> | null;

constructor(
protected options: GenerateTypescriptOptions,
Expand All @@ -42,7 +42,7 @@ export class TSResolverGenerator {
}
}

public async generate(): Promise<GenerateResolversResult> {
public generate(): GenerateResolversResult {
const { introspectionResult } = this;
const gqlTypes = introspectionResult.__schema.types.filter(type => !isBuiltinType(type));
this.queryType = introspectionResult.__schema.queryType;
Expand Down
Loading