Skip to content

Commit

Permalink
feat(router): add client sdk generation for REST/GraphQL (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael-Vol committed May 12, 2022
1 parent eeaaf2c commit 52540c8
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 7 deletions.
22 changes: 16 additions & 6 deletions packages/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,31 @@
},
"license": "ISC",
"dependencies": {
"@grpc/proto-loader": "^0.5.4",
"@conduitplatform/grpc-sdk": "^1.0.1",
"@conduitplatform/commons": "^1.0.0",
"@conduitplatform/grpc-sdk": "^1.0.1",
"@graphql-codegen/cli": "^2.6.2",
"@graphql-codegen/typescript": "^2.4.10",
"@graphql-codegen/typescript-apollo-angular": "^3.4.10",
"@graphql-codegen/typescript-operations": "^2.3.7",
"@graphql-codegen/typescript-react-apollo": "^3.2.14",
"@graphql-codegen/typescript-react-query": "^3.5.11",
"@graphql-codegen/typescript-vue-apollo": "^3.2.12",
"@graphql-codegen/typescript-vue-urql": "^2.2.11",
"@grpc/grpc-js": "^1.5.2",
"@grpc/proto-loader": "^0.5.4",
"@openapitools/openapi-generator-cli": "^2.5.1",
"apollo-server-express": "^2.11.0",
"deepdash": "^5.0.1",
"graphql": "^15.0.0",
"graphql-codegen-svelte-apollo": "^1.1.0",
"graphql-parse-resolve-info": "^4.5.0",
"graphql-tools": "^4.0.7",
"graphql-type-json": "^0.3.1",
"@grpc/grpc-js": "^1.5.2",
"lodash": "^4.17.21",
"swagger-ui-express": "4.1.5",
"redis": "^3.1.0",
"socket.io": "^4.0.1",
"socket.io-redis": "^6.1.0",
"redis": "^3.1.0"
"swagger-ui-express": "4.1.5"
},
"directories": {
"lib": "src"
Expand All @@ -37,8 +47,8 @@
],
"devDependencies": {
"@types/express": "~4.16.1",
"@types/lodash": "^4.14.149",
"@types/graphql-type-json": "^0.3.2",
"@types/lodash": "^4.14.149",
"@types/node": "^13.9.8",
"express": "~4.16.1",
"rimraf": "^3.0.2",
Expand Down
166 changes: 166 additions & 0 deletions packages/router/src/admin/routes/GenerateGraphQlClient.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {
ConduitBoolean,
ConduitError,
ConduitRoute,
ConduitRouteActions,
ConduitRouteParameters,
ConduitRouteReturnDefinition,
ConduitString,
TYPE,
} from '@conduitplatform/commons';
import { ConduitDefaultRouter } from '../..';
import { generate } from '@graphql-codegen/cli';
import path from 'path';
import { status } from '@grpc/grpc-js';
import fs, { unlink } from 'fs';
import { isEmpty } from 'lodash';
import { GrpcError } from '@conduitplatform/grpc-sdk';

export function generateGraphQlClient(router: ConduitDefaultRouter) {
return new ConduitRoute(
{
path: '/router/generate/graphql',
action: ConduitRouteActions.POST,
bodyParams: {
clientType: ConduitString.Required,
config: {
avoidOptionals: ConduitBoolean.Optional,
immutableTypes: ConduitBoolean.Optional,
preResolveTypes: ConduitBoolean.Optional,
flattenGeneratedTypes: ConduitBoolean.Optional,
enumsAsTypes: ConduitBoolean.Optional,
fragmentMasking: ConduitBoolean.Optional,
reactApolloVersion: ConduitString.Optional,
operationResultSuffix: ConduitString.Optional,
},
},
},
new ConduitRouteReturnDefinition('generateGraphQlClient', {
fileName: ConduitString.Required,
fileType: ConduitString.Required,
file: ConduitString.Required,
}),
async (request: ConduitRouteParameters) => {
const config = JSON.parse(JSON.stringify(request.params?.config ?? {}));
const { plugins, fileName } = selectPlugin(
request.params!.clientType,
Object.keys(config),
);
const outputPath = path.resolve(__dirname, `generate/${fileName}`);
try {
await generate({
schema: {
'http://localhost:3000/graphql': {
headers: {
clientid: request.headers.clientid as string,
clientsecret: request.headers.clientsecret as string,
},
},
},
generates: {
[outputPath]: {
plugins,
config,
},
},
});
const file = fs.readFileSync(outputPath).toString('base64');
unlink(outputPath, (err) => {
if (err) throw new ConduitError(err.name, status.INTERNAL, err.message);
});
return {
result: {
fileName,
fileType: 'text',
file,
},
};
} catch (error) {
throw new ConduitError(
(error as Error).name,
status.INTERNAL,
(error as Error).message
);
}
}
);
}

function selectPlugin(pluginName: string, configOptions: string[]) {
const baseOptions = [
'avoidOptionals',
'immutableTypes',
'preResolveTypes',
'flattenGeneratedTypes',
'enumsAsTypes',
'fragmentMasking',
];

switch (pluginName) {
case 'typescript':
let fileNameExtension = checkConfig(baseOptions, configOptions);
return {
plugins: ['typescript', 'typescript-operations'],
fileName: ` ${fileNameExtension}.ts`,
};
case 'react':
fileNameExtension = checkConfig(baseOptions, configOptions);
return {
plugins: ['typescript', 'typescript-operations', 'typescript-react-query'],
fileName: `types.react-query${fileNameExtension}.ts`,
};
case 'react-apollo':
fileNameExtension = checkConfig(
[...baseOptions, 'reactApolloVersion', 'operationResultSuffix'],
configOptions
);
return {
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
fileName: `types.reactApollo${fileNameExtension}.tsx`,
};
case 'angular':
fileNameExtension = checkConfig([...baseOptions], configOptions);
return {
plugins: ['typescript', 'typescript-operations', 'typescript-apollo-angular'],
fileName: `types.apolloAngular${fileNameExtension}.ts`,
};
case 'vue-urql':
fileNameExtension = checkConfig([...baseOptions], configOptions);
return {
plugins: ['typescript', 'typescript-operations', 'typescript-vue-urql'],
fileName: `types.vue-urql${fileNameExtension}.ts`,
};
case 'vue-apollo':
fileNameExtension = checkConfig([...baseOptions], configOptions);
return {
plugins: ['typescript', 'typescript-operations', 'typescript-vue-urql'],
fileName: `types.vueApollo${fileNameExtension}.ts`,
};
case 'svelte':
fileNameExtension = checkConfig([...baseOptions], configOptions);
return {
plugins: ['typescript', 'typescript-operations', 'graphql-codegen-svelte-apollo'],
fileName: `types.svelte-apollo${fileNameExtension}.ts`,
};
default:
throw new GrpcError(status.INVALID_ARGUMENT, 'Invalid Plugin');
}
}

/*
* Checks if the configuration option for the generator are valid
*/
const checkConfig = (validOptions: string[], configOptions: string[]) => {
if(isEmpty(configOptions)) {
return '';
}

const invalidOptions = configOptions.filter((e) => !validOptions.includes(e));
if (!isEmpty(invalidOptions)) {
throw new GrpcError(
status.INVALID_ARGUMENT,
`Invalid config options: ${invalidOptions.join(',')}`
);
}
return `.${configOptions.join('.')}`;
};
67 changes: 67 additions & 0 deletions packages/router/src/admin/routes/GenerateRestClient.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
ConduitBoolean,
ConduitRoute,
ConduitRouteActions,
ConduitRouteParameters,
ConduitRouteReturnDefinition,
ConduitString,
ConduitError,
TYPE,
} from '@conduitplatform/commons';
import { ConduitDefaultRouter } from '../..';
const util = require('util');
const exec = util.promisify(require('child_process').exec);
import path from 'path';
import url from 'url';
import fs, { unlink } from 'fs';
import { status } from '@grpc/grpc-js';

export function generateRestClient(router: ConduitDefaultRouter) {
return new ConduitRoute(
{
path: '/router/generate/rest',
action: ConduitRouteActions.POST,
bodyParams: {
clientType: ConduitString.Required,
isAdmin: ConduitBoolean.Optional,
},
},
new ConduitRouteReturnDefinition('generateRestClient', {
fileName: ConduitString.Required,
fileType: ConduitString.Required,
file: ConduitString.Required,
}),
async (request: ConduitRouteParameters) => {
const clientType = request.params!.clientType;
const outputPath = path.resolve(__dirname, 'generate/rest');
const inputSpec = request.params!.isAdmin ? 'admin/swagger.json' : 'swagger.json';
try {
await exec(
`openapi-generator generate -i http://localhost:${
process.env['PORT'] ?? '3000'
}/${inputSpec} -g ${clientType} -o ${outputPath} --skip-validate-spec`
);

const zipPath = path.resolve(__dirname, 'generate/rest.zip');
await exec(`zip -r ${zipPath} ${outputPath}`);
const file = fs.readFileSync(zipPath).toString('base64');
unlink(zipPath, (err) => {
if (err) throw new ConduitError(err.name, status.INTERNAL, err.message);
});
return {
result: {
fileName: `${clientType}.zip`,
fileType: 'application/zip',
file,
},
};
} catch (error) {
throw new ConduitError(
(error as Error).name,
status.INTERNAL,
(error as Error).message
);
}
}
);
}
3 changes: 2 additions & 1 deletion packages/router/src/admin/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './GetRoutes.route';
export * from './GetMiddlewares.route';

export * from './GenerateRestClient.route';
export * from './GenerateGraphQlClient.route';
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ export class ConduitDefaultRouter implements IConduitRouter {
private registerAdminRoutes() {
this._commons.getAdmin().registerRoute(adminRoutes.getRoutes(this));
this._commons.getAdmin().registerRoute(adminRoutes.getMiddlewares(this));
this._commons.getAdmin().registerRoute(adminRoutes.generateRestClient(this));
this._commons.getAdmin().registerRoute(adminRoutes.generateGraphQlClient(this));
}

setConfig(moduleConfig: any) {
Expand Down

0 comments on commit 52540c8

Please sign in to comment.