UNPKG

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