UNPKG

16.7 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 resources = {
44 ...resources,
45 ...{[key + 'CreateResolver']: this.makeCreateRelationalResolver(key)},
46 ...{[key + 'GetResolver']: this.makeGetRelationalResolver(key)},
47 ...{[key + 'UpdateResolver']: this.makeUpdateRelationalResolver(key)},
48 ...{[key + 'DeleteResolver']: this.makeDeleteRelationalResolver(key)},
49 ...{[key + 'ListResolver']: this.makeListRelationalResolver(key)},
50 }
51 // TODO: Add Guesstimate Query Resolvers
52 })
53
54 return resources
55 }
56
57 /**
58 * Private Helpers to Generate the CFN Spec for the Resolver Resources
59 */
60
61 /**
62 * Creates and returns the CFN Spec for the 'Create' Resolver Resource provided
63 * a GraphQL Type as the input
64 *
65 * @param type - the graphql type for which the create resolver will be created
66 * @param mutationTypeName - will be 'Mutation'
67 */
68 private makeCreateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') {
69 const fieldName = graphqlName('create' + toUpper(type))
70 let createSql = `INSERT INTO ${type} $colStr VALUES $valStr`
71 let selectSql =
72 `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.create${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}`
73
74 const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`
75 const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`
76
77 const reqTemplate = print(
78 compoundExpression([
79 set(ref('cols'), list([])),
80 set(ref('vals'), list([])),
81 forEach(
82 ref('entry'),
83 ref(`ctx.args.create${toUpper(type)}Input.keySet()`),
84 [
85 set(ref('discard'), ref(`cols.add($entry)`)),
86 set(ref('discard'), ref(`vals.add("'$ctx.args.create${toUpper(type)}Input[$entry]'")`))
87 ]
88 ),
89 set(ref('valStr'), ref('vals.toString().replace("[","(").replace("]",")")')),
90 set(ref('colStr'), ref('cols.toString().replace("[","(").replace("]",")")')),
91 RelationalDBMappingTemplate.rdsQuery({
92 statements: list([str(createSql), str(selectSql)])
93 })
94 ])
95 )
96
97 const resTemplate = print(
98 ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])')
99 )
100
101 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
102 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
103
104 let resolver = new AppSync.Resolver ({
105 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
106 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
107 TypeName: mutationTypeName,
108 FieldName: fieldName,
109 RequestMappingTemplateS3Location: Fn.Sub(
110 s3BaseUrl,
111 {
112 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
113 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
114 [resolverFileName]: reqFileName
115 }
116 ),
117 ResponseMappingTemplateS3Location: Fn.Sub(
118 s3BaseUrl,
119 {
120 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
121 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
122 [resolverFileName]: resFileName
123 }
124 )
125 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
126 return resolver
127 }
128
129 /**
130 * Creates and Returns the CFN Spec for the 'Get' Resolver Resource provided
131 * a GraphQL type
132 *
133 * @param type - the graphql type for which the get resolver will be created
134 * @param queryTypeName - will be 'Query'
135 */
136 private makeGetRelationalResolver(type: string, queryTypeName: string = 'Query') {
137 const fieldName = graphqlName('get' + toUpper(type))
138 let sql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`
139 const reqFileName = `${queryTypeName}.${fieldName}.req.vtl`
140 const resFileName = `${queryTypeName}.${fieldName}.res.vtl`
141
142 const reqTemplate = print(
143 compoundExpression([
144 RelationalDBMappingTemplate.rdsQuery({
145 statements: list([str(sql)])
146 })
147 ])
148 )
149
150 const resTemplate = print(
151 ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])')
152 )
153
154 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
155 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
156
157 let resolver = new AppSync.Resolver ({
158 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
159 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
160 FieldName: fieldName,
161 TypeName: queryTypeName,
162 RequestMappingTemplateS3Location: Fn.Sub(
163 s3BaseUrl,
164 {
165 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
166 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
167 [resolverFileName]: reqFileName
168 }
169 ),
170 ResponseMappingTemplateS3Location: Fn.Sub(
171 s3BaseUrl,
172 {
173 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
174 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
175 [resolverFileName]: resFileName
176 }
177 )
178 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
179 return resolver
180 }
181
182 /**
183 * Creates and Returns the CFN Spec for the 'Update' Resolver Resource provided
184 * a GraphQL type
185 *
186 * @param type - the graphql type for which the update resolver will be created
187 * @param mutationTypeName - will be 'Mutation'
188 */
189 private makeUpdateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') {
190 const fieldName = graphqlName('update' + toUpper(type))
191 const updateSql =
192 `UPDATE ${type} SET $update WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}`
193 const selectSql =
194 `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}`
195 const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`
196 const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`
197
198 const reqTemplate = print(
199 compoundExpression([
200 set(ref('updateList'), obj({})),
201 forEach(
202 ref('entry'),
203 ref(`ctx.args.update${toUpper(type)}Input.keySet()`),
204 [
205 set(ref('discard'), ref(`updateList.put($entry, "'$ctx.args.update${toUpper(type)}Input[$entry]'")`))
206 ]
207 ),
208 set(ref('update'), ref(`updateList.toString().replace("{","").replace("}","")`)),
209 RelationalDBMappingTemplate.rdsQuery({
210 statements: list([str(updateSql), str(selectSql)])
211 })
212 ])
213 )
214
215 const resTemplate = print(
216 ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])')
217 )
218
219 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
220 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
221
222 let resolver = new AppSync.Resolver ({
223 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
224 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
225 TypeName: mutationTypeName,
226 FieldName: fieldName,
227 RequestMappingTemplateS3Location: Fn.Sub(
228 s3BaseUrl,
229 {
230 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
231 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
232 [resolverFileName]: reqFileName
233 }
234 ),
235 ResponseMappingTemplateS3Location: Fn.Sub(
236 s3BaseUrl,
237 {
238 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
239 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
240 [resolverFileName]: resFileName
241 }
242 )
243 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
244 return resolver
245 }
246
247 /**
248 * Creates and Returns the CFN Spec for the 'Delete' Resolver Resource provided
249 * a GraphQL type
250 *
251 * @param type - the graphql type for which the delete resolver will be created
252 * @param mutationTypeName - will be 'Mutation'
253 */
254 private makeDeleteRelationalResolver(type: string, mutationTypeName: string = 'Mutation') {
255 const fieldName = graphqlName('delete' + toUpper(type))
256 const selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`
257 const deleteSql = `DELETE FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`
258 const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`
259 const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`
260 const reqTemplate = print(
261 compoundExpression([
262 RelationalDBMappingTemplate.rdsQuery({
263 statements: list([str(selectSql), str(deleteSql)])
264 })
265 ])
266 )
267 const resTemplate = print(
268 ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])')
269 )
270
271 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
272 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
273
274 let resolver = new AppSync.Resolver ({
275 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
276 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
277 TypeName: mutationTypeName,
278 FieldName: fieldName,
279 RequestMappingTemplateS3Location: Fn.Sub(
280 s3BaseUrl,
281 {
282 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
283 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
284 [resolverFileName]: reqFileName
285 }
286 ),
287 ResponseMappingTemplateS3Location: Fn.Sub(
288 s3BaseUrl,
289 {
290 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
291 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
292 [resolverFileName]: resFileName
293 }
294 )
295 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
296
297 return resolver
298 }
299
300 /**
301 * Creates and Returns the CFN Spec for the 'List' Resolver Resource provided
302 * a GraphQL type
303 *
304 * @param type - the graphql type for which the list resolver will be created
305 * @param queryTypeName - will be 'Query'
306 */
307 private makeListRelationalResolver(type: string, queryTypeName: string = 'Query') {
308 const fieldName = graphqlName('list' + plurality(toUpper(type)))
309 const sql = `SELECT * FROM ${type}`
310 const reqFileName = `${queryTypeName}.${fieldName}.req.vtl`
311 const resFileName = `${queryTypeName}.${fieldName}.res.vtl`
312 const reqTemplate = print(
313 RelationalDBMappingTemplate.rdsQuery({
314 statements: list([str(sql)])
315 })
316 )
317 const resTemplate = print(
318 ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0])')
319 )
320
321 fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8');
322 fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8');
323
324 let resolver = new AppSync.Resolver ({
325 ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId),
326 DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'),
327 TypeName: queryTypeName,
328 FieldName: fieldName,
329 RequestMappingTemplateS3Location: Fn.Sub(
330 s3BaseUrl,
331 {
332 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
333 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
334 [resolverFileName]: reqFileName
335 }
336 ),
337 ResponseMappingTemplateS3Location: Fn.Sub(
338 s3BaseUrl,
339 {
340 [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket),
341 [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey),
342 [resolverFileName]: resFileName
343 }
344 )
345 }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource])
346
347 return resolver
348 }
349}
\No newline at end of file