Skip to content

Commit

Permalink
ts support
Browse files Browse the repository at this point in the history
  • Loading branch information
dotansimha committed Feb 23, 2020
1 parent 72412a6 commit 6a36fb1
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 54 deletions.
3 changes: 1 addition & 2 deletions examples/location-weather/.gitignore
@@ -1,2 +1 @@
src/mesh/schema/
src/mesh/sdk.ts
__generated__
6 changes: 5 additions & 1 deletion examples/location-weather/package.json
Expand Up @@ -7,7 +7,11 @@
"dev": "ts-node-dev src/index.ts",
"test": "jest",
"build": "tsc",
"serve:mesh": "graphql-mesh serve"
"prestart": "yarn mesh:ts",
"predev": "yarn mesh:ts",
"premesh:serve": "yarn mesh:ts",
"mesh:serve": "graphql-mesh serve",
"mesh:ts": "graphql-mesh typescript --output ./src/__generated__/mesh.ts"
},
"devDependencies": {
"@types/node": "13.7.4",
Expand Down
12 changes: 7 additions & 5 deletions examples/location-weather/src/mesh/additional-resolvers.ts
@@ -1,6 +1,8 @@
export const resolvers = {
import { Resolvers } from '../__generated__/mesh';

export const resolvers: Resolvers = {
PopulatedPlaceSummary: {
dailyForecast: async (placeSummary: any, args: never, { Weather }: any) => {
dailyForecast: async (placeSummary, args: never, { Weather }) => {
const forecast = await Weather.api.getForecastDailyLatLatLonLon({
lat: placeSummary.latitude,
lon: placeSummary.longitude,
Expand All @@ -9,14 +11,14 @@ export const resolvers = {

return forecast.data;
},
todayForecast: async (placeSummary: any, args: never, { Weather }: any) => {
todayForecast: async (placeSummary, args: never, { Weather }) => {
const forecast = await Weather.api.getForecastDailyLatLatLonLon({
lat: placeSummary.latitude,
lon: placeSummary.longitude,
key: Weather.config.apiKey,
key: Weather.config.apiKey
});

return forecast.data[0];
}
}
};
};
2 changes: 2 additions & 0 deletions packages/cli/package.json
Expand Up @@ -36,6 +36,8 @@
"definition": "dist/index.d.ts"
},
"dependencies": {
"@graphql-codegen/typescript": "1.12.2",
"@graphql-codegen/typescript-resolvers": "1.12.2",
"apollo-server": "2.10.1",
"chalk": "3.0.0",
"js-yaml": "3.13.1",
Expand Down
32 changes: 15 additions & 17 deletions packages/cli/src/bin.ts
@@ -1,9 +1,13 @@
#!/usr/bin/env node -r ts-node/register/transpile-only

import { ApolloServer } from 'apollo-server';
import { writeFileSync } from 'fs';
import { parseConfig, getMesh } from '@graphql-mesh/runtime';
import * as yargs from 'yargs';
import { createLogger, format, transports } from 'winston';
import { generateTsTypes } from './commands/typescript';
import { serveMesh } from './commands/serve';
import { resolve, dirname } from 'path';
import * as mkdirp from 'mkdirp';

const logger = createLogger({
level: 'info',
Expand All @@ -18,41 +22,35 @@ const logger = createLogger({
export async function graphqlMesh() {
// TODO: Add flag for fetching specific config file and not from default path
const meshConfig = await parseConfig();
const { schema, contextBuilder } = await getMesh(meshConfig);
const { schema, contextBuilder, rawSources } = await getMesh(meshConfig);

yargs
.command<{ verbose: boolean }>(
'serve',
'Serves a GraphiQLApolloServer interface to test your Mesh API',
() => null,
async () => {
const server = new ApolloServer({
schema,
context: () => {
const context = contextBuilder();
return context;
}
});

server.listen().then(({ url }) => {
console.log(`🕸️ Serving GraphQL Mesh GraphiQL: ${url}`);
});
await serveMesh(logger, schema, contextBuilder);
}
)
.command<{ verbose: boolean }>(
'generate-sdk',
'Generates fully type-safe SDK based on unifid GraphQL schema and GraphQL operations',
() => null,
async args => {
// TODO
// TODO: Generate SDK based on operations
}
)
.command<{ verbose: boolean }>(
.command<{ verbose: boolean, output: string }>(
'typescript',
'Generates TypeScript typings for the generated mesh',
() => null,
async args => {
// TODO
async (args) => {
const result = await generateTsTypes(logger, schema, rawSources);
const outFile = resolve(process.cwd(), args.output);
const dirName = dirname(outFile);
mkdirp.sync(dirName);
writeFileSync(outFile, result);
}
)
.option('verbose', {
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/commands/serve.ts
@@ -0,0 +1,21 @@
import { GraphQLSchema } from 'graphql';
import { ApolloServer } from 'apollo-server';
import { Logger } from 'winston';

export function serveMesh(
logger: Logger,
schema: GraphQLSchema,
contextBuilder: () => Record<string, any>
) {
const server = new ApolloServer({
schema,
context: () => {
const context = contextBuilder();
return context;
}
});

server.listen().then(({ url }) => {
logger.info(`🕸️ Serving GraphQL Mesh GraphiQL: ${url}`);
});
}
82 changes: 82 additions & 0 deletions packages/cli/src/commands/typescript.ts
@@ -0,0 +1,82 @@
import { Logger } from 'winston';
import { RawSourcesOutput } from '@graphql-mesh/runtime';
import { plugin as tsBasePlugin } from '@graphql-codegen/typescript';
import { plugin as tsResolversPlugin } from '@graphql-codegen/typescript-resolvers';
import { Types } from '@graphql-codegen/plugin-helpers';
import { GraphQLSchema } from 'graphql';

export async function generateTsTypes(
logger: Logger,
unifiedSchema: GraphQLSchema,
rawSources: RawSourcesOutput
): Promise<string> {
const unifiedContextIdentifier = 'MeshContext';

const tsTypes = (await tsBasePlugin(
unifiedSchema,
[],
{}
)) as Types.ComplexPluginOutput;
const tsResolversTypes = (await tsResolversPlugin(unifiedSchema, [], {
useIndexSignature: true,
noSchemaStitching: true,
contextType: unifiedContextIdentifier,
federation: false
})) as Types.ComplexPluginOutput;

const results = await Promise.all(
Object.keys(rawSources).map(async apiName => {
const source = rawSources[apiName];

if (source.handler.tsSupport) {
return await source.handler.tsSupport({
schema: source.schema,
getMeshSourcePayload: source.meshSourcePayload,
name: apiName
});
} else {
logger.warn(
`Mesh source ${apiName} doesn't support TypeScript, ignoring...`
);
}
})
);

let sdkItems: string[] = [];
let contextItems: string[] = [];
let models: string[] = [];

for (const item of results) {
if (item) {
if (item.sdk) {
sdkItems.push(item.sdk.codeAst);
}
if (item.context) {
contextItems.push(item.context.codeAst);
}
if (item.models) {
contextItems.push(item.models);
}
}
}

const contextType = `export type ${unifiedContextIdentifier} = ${results
.filter(r => r && r.context)
.map(r => r?.context?.identifier)
.join(' & ')};`;

return [
...(tsTypes.prepend || []),
...(tsResolversTypes.prepend || []),
tsTypes.content,
tsResolversTypes.content,
...models,
...sdkItems,
...contextItems,
contextType,
...(tsTypes.append || []),
...(tsResolversTypes.append || [])
]
.filter(Boolean)
.join('\n\n');
}
2 changes: 1 addition & 1 deletion packages/handlers/openapi/package.json
Expand Up @@ -25,7 +25,7 @@
"typescript": "3.8.2"
},
"dependencies": {
"openapi-to-graphql": "2.0.0",
"@dotansimha/openapi-to-graphql": "2.0.1",
"@graphql-mesh/types": "0.0.11",
"@graphql-mesh/utils": "0.0.11",
"change-case": "4.1.1",
Expand Down
53 changes: 44 additions & 9 deletions packages/handlers/openapi/src/index.ts
Expand Up @@ -3,21 +3,52 @@ import { resolve } from 'path';
import isUrl from 'is-url';
import * as yaml from 'js-yaml';
import request from 'request-promise-native';
import { createGraphQLSchema } from 'openapi-to-graphql';
import { Oas3 } from 'openapi-to-graphql/lib/types/oas3';
import { Options } from 'openapi-to-graphql/lib/types/options';
import { createGraphQLSchema } from '@dotansimha/openapi-to-graphql';
import { Oas3 } from '@dotansimha/openapi-to-graphql/lib/types/oas3';
import { Options } from '@dotansimha/openapi-to-graphql/lib/types/options';
import { PreprocessingData } from '@dotansimha/openapi-to-graphql/lib/types/preprocessing_data';
import { MeshHandlerLibrary } from '@graphql-mesh/types';
import { GraphQLObjectType } from 'graphql';
import Maybe from 'graphql/tsutils/Maybe';
import { pascalCase } from 'change-case';

const handler: MeshHandlerLibrary<Options, Oas3> = {
const handler: MeshHandlerLibrary<
Options,
{ oas: Oas3; preprocessingData: PreprocessingData }
> = {
async tsSupport(options) {
console.log(options.getMeshSourcePayload);
const sdkIdentifier = `${options.name}Sdk`;
const contextIdentifier = `${options.name}Context`;
const operations =
options.getMeshSourcePayload.preprocessingData.operations;

return {};
const sdk = {
identifier: sdkIdentifier,
codeAst: `export type ${sdkIdentifier} = {
${Object.keys(operations)
.map(operationName => {
const operation = operations[operationName];
const operationGqlBaseType = operation.method === 'get' ? options.schema.getQueryType()?.name : options.schema.getMutationType()?.name;
const argsName = `${operationGqlBaseType}${pascalCase(operation.operationId)}Args`;
return ` ${operation.operationId}: (args: ${argsName}) => Promise<${pascalCase(operation.responseDefinition.graphQLTypeName)}>`;
})
.join(',\n')}
};`
};

const context = {
identifier: contextIdentifier,
codeAst: `export type ${contextIdentifier} = { ${options.name}: { config: Record<string, any>, api: ${sdkIdentifier} } };`
};

return {
sdk,
context
};
},
async getMeshSource({ filePathOrUrl, name, config }) {
let spec = null;
let spec: Oas3;

// Load from a url or from a local file. only json supported at the moment.
// I think `getValidOAS3` should support loading YAML files easily
Expand All @@ -31,8 +62,9 @@ const handler: MeshHandlerLibrary<Options, Oas3> = {
spec = readFile(actualPath);
}

const { schema } = await createGraphQLSchema(spec, {
const { schema, data } = await createGraphQLSchema(spec, {
...(config || {}),
operationIdFieldNames: true,
viewer: false // Viewer set to false in order to force users to specify auth via config file
});

Expand All @@ -50,7 +82,10 @@ const handler: MeshHandlerLibrary<Options, Oas3> = {
name,
source: filePathOrUrl
},
payload: spec
payload: {
oas: spec,
preprocessingData: data
}
};
}
};
Expand Down
6 changes: 4 additions & 2 deletions packages/runtime/src/get-mesh.ts
Expand Up @@ -5,7 +5,7 @@ import {
applySchemaTransformations,
applyOutputTransformations
} from './utils';
import { MeshSource } from '@graphql-mesh/types';
import { MeshSource, MeshHandlerLibrary } from '@graphql-mesh/types';
import { addResolveFunctionsToSchema } from 'graphql-tools-fork';

export type RawSourcesOutput = Record<
Expand All @@ -15,6 +15,7 @@ export type RawSourcesOutput = Record<
schema: GraphQLSchema;
context: Record<string, any>;
meshSourcePayload: any;
handler: MeshHandlerLibrary;
}
>;

Expand Down Expand Up @@ -49,7 +50,8 @@ export async function getMesh(
sdk: source.sdk,
schema: apiSchema,
context: apiSource.context || {},
meshSourcePayload: payload
meshSourcePayload: payload,
handler: apiSource.handler
};
}

Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/index.ts
Expand Up @@ -9,15 +9,15 @@ export type MeshSource = {

/* TS Support */
export declare type TsSupportOptions<TPayload> = {
filePathOrUrl: string;
name: string;
schema: GraphQLSchema,
getMeshSourcePayload: TPayload;
};

export type TsSupportOutput = {
sdk?: ExportedTSType;
context?: ExportedTSType;
models?: Record<string, ExportedTSType>;
models?: string;
};

export type ExportedTSType = {
Expand Down

0 comments on commit 6a36fb1

Please sign in to comment.