UNPKG

16.8 kBPlain TextView Raw
1import TemplateContext from "./RelationalDBSchemaTransformer";
2import { DocumentNode } from 'graphql'
3import { Fn } from 'cloudform'
4import AppSync from 'cloudform-types/types/appSync'
5import { print, obj, set, str, list, forEach, ref, compoundExpression } from 'graphql-mapping-template'
6import { graphqlName, toUpper, plurality } from 'graphql-transformer-common'
7import { ResourceConstants } from './ResourceConstants'
8import RelationalDBMappingTemplate from './RelationalDBMappingTemplate'
9import * as fs from 'fs-extra'
10
11const s3BaseUrl = 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}'
12const resolverFileName = 'ResolverFileName'
13/**
14 * This Class is responsible for Generating the RDS Resolvers based on the
15 * GraphQL Schema + Metadata of the RDS Cluster (i.e. Primary Keys for Tables).
16 *
17 * It will generate the CRUDL+Q (Create, Retrieve, Update, Delete, List + Queries) Resolvers as
18 * Cloudform Resources so that they may be added on to the base template that the
19 * RelationDBTemplateGenerator creates.
20 */
21export default class RelationalDBResolverGenerator {
22 document: DocumentNode
23 typePrimaryKeyMap: Map<string, string>;
24 stringFieldMap: Map<string, string[]>
25 intFieldMap: Map<string, string[]>
26 resolverFilePath: string
27
28 constructor(context: TemplateContext) {
29 this.document = context.schemaDoc
30 this.typePrimaryKeyMap = context.typePrimaryKeyMap
31 this.stringFieldMap = context.stringFieldMap
32 this.intFieldMap = context.intFieldMap
33 }
34
35 /**
36 * Creates the CRUDL+Q Resolvers as a Map of Cloudform Resources. The output can then be
37 * merged with an existing Template's map of Resources.
38 */
39 public createRelationalResolvers(resolverFilePath: string) {
40 let resources = {}
41 this.resolverFilePath = resolverFilePath
42 this.typePrimaryKeyMap.forEach((value: string, key: string) => {
43 const resourceName = key.replace(/[^A-Za-z0-9]/g, '')
44 resources = {
45 ...resources,
46 ...{[resourceName + 'CreateResolver']: this.makeCreateRelationalResolver(key)},
47 ...{[resourceName + 'GetResolver']: this.makeGetRelationalResolver(key)},
48 ...{[resourceName + 'UpdateResolver']: this.makeUpdateRelationalResolver(key)},
49 ...{[resourceName + 'DeleteResolver']: this.makeDeleteRelationalResolver(key)},
50 ...{[resourceName + 'ListResolver']: this.makeListRelationalResolver(key)},
51 }
52 // TODO: Add Guesstimate Query Resolvers
53 })
54
55 return resources
56 }
57
58 /**
59 * Private Helpers to Generate the CFN Spec for the Resolver Resources
60 */
61
62 /**
63 * Creates and returns the CFN Spec for the 'Create' Resolver Resource provided
64 * a GraphQL Type as the input
65 *
66 * @param type - the graphql type for which the create resolver will be created
67 * @param mutationTypeName - will be 'Mutation'
68 */
69 private makeCreateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') {
70 const fieldName = graphqlName('create' + toUpper(type))
71 let createSql = `INSERT INTO ${type} $colStr VALUES $valStr`
72 let selectSql =
73 `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.create${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}`
74
75 const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`
76 const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`
77
78 const reqTemplate = print(
79 compoundExpression([
80 set(ref('cols'), list([])),
81 set(ref('vals'), list([])),
82 forEach(
83 ref('entry'),
84 ref(`ctx.args.create${toUpper(type)}Input.keySet()`),
85 [
86 set(ref('discard'), ref(`cols.add($entry)`)),
87 set(ref('discard'), ref(`vals.add("'$ctx.args.create${toUpper(type)}Input[$entry]'")`))
88 ]
89 ),
90 set(ref('valStr'), ref('vals.toString().replace("[","(").replace("]",")")')),
91 set(ref('colStr'), ref('cols.toString().replace("[","(").replace("]",")")')),
92 RelationalDBMappingTemplate.rdsQuery({
93 statements: list([str(createSql), str(selectSql)])
94 })
95 ])
96 )
97
98 const resTemplate = print(
99 ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])')
100 )
101
102 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
103 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
104
105 let resolver = new AppSync.Resolver ({
106 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
107 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
108 TypeName: mutationTypeName,
109 FieldName: fieldName,
110 RequestMappingTemplateS3Location: Fn.Sub(
111 s3BaseUrl,
112 {
113 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
114 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
115 [resolverFileName]: reqFileName
116 }
117 ),
118 ResponseMappingTemplateS3Location: Fn.Sub(
119 s3BaseUrl,
120 {
121 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
122 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
123 [resolverFileName]: resFileName
124 }
125 )
126 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
127 return resolver
128 }
129
130 /**
131 * Creates and Returns the CFN Spec for the 'Get' Resolver Resource provided
132 * a GraphQL type
133 *
134 * @param type - the graphql type for which the get resolver will be created
135 * @param queryTypeName - will be 'Query'
136 */
137 private makeGetRelationalResolver(type: string, queryTypeName: string = 'Query') {
138 const fieldName = graphqlName('get' + toUpper(type))
139 let sql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`
140 const reqFileName = `${queryTypeName}.${fieldName}.req.vtl`
141 const resFileName = `${queryTypeName}.${fieldName}.res.vtl`
142
143 const reqTemplate = print(
144 compoundExpression([
145 RelationalDBMappingTemplate.rdsQuery({
146 statements: list([str(sql)])
147 })
148 ])
149 )
150
151 const resTemplate = print(
152 ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])')
153 )
154
155 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
156 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
157
158 let resolver = new AppSync.Resolver ({
159 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
160 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
161 FieldName: fieldName,
162 TypeName: queryTypeName,
163 RequestMappingTemplateS3Location: Fn.Sub(
164 s3BaseUrl,
165 {
166 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
167 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
168 [resolverFileName]: reqFileName
169 }
170 ),
171 ResponseMappingTemplateS3Location: Fn.Sub(
172 s3BaseUrl,
173 {
174 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
175 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
176 [resolverFileName]: resFileName
177 }
178 )
179 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
180 return resolver
181 }
182
183 /**
184 * Creates and Returns the CFN Spec for the 'Update' Resolver Resource provided
185 * a GraphQL type
186 *
187 * @param type - the graphql type for which the update resolver will be created
188 * @param mutationTypeName - will be 'Mutation'
189 */
190 private makeUpdateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') {
191 const fieldName = graphqlName('update' + toUpper(type))
192 const updateSql =
193 `UPDATE ${type} SET $update WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}`
194 const selectSql =
195 `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}`
196 const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`
197 const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`
198
199 const reqTemplate = print(
200 compoundExpression([
201 set(ref('updateList'), obj({})),
202 forEach(
203 ref('entry'),
204 ref(`ctx.args.update${toUpper(type)}Input.keySet()`),
205 [
206 set(ref('discard'), ref(`updateList.put($entry, "'$ctx.args.update${toUpper(type)}Input[$entry]'")`))
207 ]
208 ),
209 set(ref('update'), ref(`updateList.toString().replace("{","").replace("}","")`)),
210 RelationalDBMappingTemplate.rdsQuery({
211 statements: list([str(updateSql), str(selectSql)])
212 })
213 ])
214 )
215
216 const resTemplate = print(
217 ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])')
218 )
219
220 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
221 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
222
223 let resolver = new AppSync.Resolver ({
224 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
225 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
226 TypeName: mutationTypeName,
227 FieldName: fieldName,
228 RequestMappingTemplateS3Location: Fn.Sub(
229 s3BaseUrl,
230 {
231 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
232 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
233 [resolverFileName]: reqFileName
234 }
235 ),
236 ResponseMappingTemplateS3Location: Fn.Sub(
237 s3BaseUrl,
238 {
239 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
240 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
241 [resolverFileName]: resFileName
242 }
243 )
244 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
245 return resolver
246 }
247
248 /**
249 * Creates and Returns the CFN Spec for the 'Delete' Resolver Resource provided
250 * a GraphQL type
251 *
252 * @param type - the graphql type for which the delete resolver will be created
253 * @param mutationTypeName - will be 'Mutation'
254 */
255 private makeDeleteRelationalResolver(type: string, mutationTypeName: string = 'Mutation') {
256 const fieldName = graphqlName('delete' + toUpper(type))
257 const selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`
258 const deleteSql = `DELETE FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`
259 const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`
260 const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`
261 const reqTemplate = print(
262 compoundExpression([
263 RelationalDBMappingTemplate.rdsQuery({
264 statements: list([str(selectSql), str(deleteSql)])
265 })
266 ])
267 )
268 const resTemplate = print(
269 ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])')
270 )
271
272 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
273 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
274
275 let resolver = new AppSync.Resolver ({
276 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
277 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
278 TypeName: mutationTypeName,
279 FieldName: fieldName,
280 RequestMappingTemplateS3Location: Fn.Sub(
281 s3BaseUrl,
282 {
283 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
284 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
285 [resolverFileName]: reqFileName
286 }
287 ),
288 ResponseMappingTemplateS3Location: Fn.Sub(
289 s3BaseUrl,
290 {
291 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
292 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
293 [resolverFileName]: resFileName
294 }
295 )
296 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
297
298 return resolver
299 }
300
301 /**
302 * Creates and Returns the CFN Spec for the 'List' Resolver Resource provided
303 * a GraphQL type
304 *
305 * @param type - the graphql type for which the list resolver will be created
306 * @param queryTypeName - will be 'Query'
307 */
308 private makeListRelationalResolver(type: string, queryTypeName: string = 'Query') {
309 const fieldName = graphqlName('list' + plurality(toUpper(type)))
310 const sql = `SELECT * FROM ${type}`
311 const reqFileName = `${queryTypeName}.${fieldName}.req.vtl`
312 const resFileName = `${queryTypeName}.${fieldName}.res.vtl`
313 const reqTemplate = print(
314 RelationalDBMappingTemplate.rdsQuery({
315 statements: list([str(sql)])
316 })
317 )
318 const resTemplate = print(
319 ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0])')
320 )
321
322 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
323 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
324
325 let resolver = new AppSync.Resolver ({
326 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
327 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
328 TypeName: queryTypeName,
329 FieldName: fieldName,
330 RequestMappingTemplateS3Location: Fn.Sub(
331 s3BaseUrl,
332 {
333 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
334 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
335 [resolverFileName]: reqFileName
336 }
337 ),
338 ResponseMappingTemplateS3Location: Fn.Sub(
339 s3BaseUrl,
340 {
341 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
342 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
343 [resolverFileName]: resFileName
344 }
345 )
346 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
347
348 return resolver
349 }
350}
\No newline at end of file