-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(router): add client sdk generation for REST/GraphQL (#139)
- Loading branch information
1 parent
eeaaf2c
commit 52540c8
Showing
5 changed files
with
253 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
packages/router/src/admin/routes/GenerateGraphQlClient.route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
67
packages/router/src/admin/routes/GenerateRestClient.route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters