UNPKG

10.7 kBPlain TextView Raw
1import { Kind, ObjectTypeDefinitionNode, SchemaDefinitionNode, InputObjectTypeDefinitionNode, DocumentNode } from 'graphql';
2import {
3 getNamedType,
4 getOperationFieldDefinition,
5 getNonNullType,
6 getInputValueDefinition,
7 getTypeDefinition,
8 getFieldDefinition,
9 getDirectiveNode,
10 getOperationTypeDefinition,
11} from './RelationalDBSchemaTransformerUtils';
12import { RelationalDBParsingException } from './RelationalDBParsingException';
13import { IRelationalDBReader } from './IRelationalDBReader';
14import { toUpper } from 'graphql-transformer-common';
15
16/**
17 * This class is used to transition all of the columns and key metadata from a table for use
18 * in generating appropriate GraphQL schema structures. It will track type definitions for
19 * the base table, update mutation inputs, create mutation inputs, and primary key metadata.
20 */
21export class TableContext {
22 tableTypeDefinition: ObjectTypeDefinitionNode;
23 createTypeDefinition: InputObjectTypeDefinitionNode;
24 updateTypeDefinition: InputObjectTypeDefinitionNode;
25 // Table primary key metadata, to help properly key queries and mutations.
26 tableKeyField: string;
27 tableKeyFieldType: string;
28 stringFieldList: string[];
29 intFieldList: string[];
30 constructor(
31 typeDefinition: ObjectTypeDefinitionNode,
32 createDefinition: InputObjectTypeDefinitionNode,
33 updateDefinition: InputObjectTypeDefinitionNode,
34 primaryKeyField: string,
35 primaryKeyType: string,
36 stringFieldList: string[],
37 intFieldList: string[]
38 ) {
39 this.tableTypeDefinition = typeDefinition;
40 this.tableKeyField = primaryKeyField;
41 this.createTypeDefinition = createDefinition;
42 this.updateTypeDefinition = updateDefinition;
43 this.tableKeyFieldType = primaryKeyType;
44 this.stringFieldList = stringFieldList;
45 this.intFieldList = intFieldList;
46 }
47}
48
49/**
50 * This class is used to transition all of the information needed to generate the
51 * CloudFormation template. This is the class that is outputted by the SchemaTransformer and the one that
52 * RelationalDBTemplateGenerator takes in for the constructor. It tracks the graphql schema document,
53 * map of the primary keys for each of the types. It is also being used to track the CLI inputs needed
54 * for DataSource Creation, as data source creation is apart of the cfn template generation.
55 */
56export class TemplateContext {
57 schemaDoc: DocumentNode;
58 typePrimaryKeyMap: Map<string, string>;
59 typePrimaryKeyTypeMap: Map<string, string>;
60 stringFieldMap: Map<string, string[]>;
61 intFieldMap: Map<string, string[]>;
62 secretStoreArn: string;
63 rdsClusterIdentifier: string;
64 databaseName: string;
65 databaseSchema: string;
66 region: string;
67
68 constructor(
69 schemaDoc: DocumentNode,
70 typePrimaryKeyMap: Map<string, string>,
71 stringFieldMap: Map<string, string[]>,
72 intFieldMap: Map<string, string[]>,
73 typePrimaryKeyTypeMap?: Map<string, string>
74 ) {
75 this.schemaDoc = schemaDoc;
76 this.typePrimaryKeyMap = typePrimaryKeyMap;
77 this.stringFieldMap = stringFieldMap;
78 this.intFieldMap = intFieldMap;
79 this.typePrimaryKeyTypeMap = typePrimaryKeyTypeMap;
80 }
81}
82
83export class RelationalDBSchemaTransformer {
84 dbReader: IRelationalDBReader;
85 database: string;
86
87 constructor(dbReader: IRelationalDBReader, database: string) {
88 this.dbReader = dbReader;
89 this.database = database;
90 }
91
92 public introspectDatabaseSchema = async (): Promise<TemplateContext> => {
93 // Get all of the tables within the provided db
94 let tableNames = null;
95 try {
96 tableNames = await this.dbReader.listTables();
97 } catch (err) {
98 throw new RelationalDBParsingException(`Failed to list tables in ${this.database}`, err.stack);
99 }
100
101 let typeContexts = new Array();
102 let types = new Array();
103 let pkeyMap = new Map<string, string>();
104 let pkeyTypeMap = new Map<string, string>();
105 let stringFieldMap = new Map<string, string[]>();
106 let intFieldMap = new Map<string, string[]>();
107
108 for (const tableName of tableNames) {
109 let type: TableContext = null;
110 try {
111 type = await this.dbReader.describeTable(tableName);
112 } catch (err) {
113 throw new RelationalDBParsingException(`Failed to describe table ${tableName}`, err.stack);
114 }
115
116 // NOTE from @mikeparisstuff. The GraphQL schema generation breaks
117 // when the table does not have an explicit primary key.
118 if (type.tableKeyField) {
119 typeContexts.push(type);
120 // Generate the 'connection' type for each table type definition
121 // TODO: Determine if Connection is needed as Data API doesn't provide pagination
122 // TODO: As we add different db sources, we should conditionally do this even if we don't for Aurora serverless.
123 //types.push(this.getConnectionType(tableName))
124 // Generate the create operation input for each table type definition
125 types.push(type.createTypeDefinition);
126 // Generate the default shape for the table's structure
127 types.push(type.tableTypeDefinition);
128 // Generate the update operation input for each table type definition
129 types.push(type.updateTypeDefinition);
130
131 // Update the field map with the new field lists for the current table
132 stringFieldMap.set(tableName, type.stringFieldList);
133 intFieldMap.set(tableName, type.intFieldList);
134 pkeyMap.set(tableName, type.tableKeyField);
135 pkeyTypeMap.set(tableName, type.tableKeyFieldType);
136 } else {
137 console.warn(`Skipping table ${type.tableTypeDefinition.name.value} because it does not have a single PRIMARY KEY.`);
138 }
139 }
140
141 // Generate the mutations and queries based on the table structures
142 types.push(this.getMutations(typeContexts));
143 types.push(this.getQueries(typeContexts));
144 types.push(this.getSubscriptions(typeContexts));
145 types.push(this.getSchemaType());
146
147 let context = this.dbReader.hydrateTemplateContext(
148 new TemplateContext({ kind: Kind.DOCUMENT, definitions: types }, pkeyMap, stringFieldMap, intFieldMap, pkeyTypeMap)
149 );
150 return context;
151 };
152
153 /**
154 * Creates a schema type definition node, including operations for each of query, mutation, and subscriptions.
155 *
156 * @returns a basic schema definition node.
157 */
158 getSchemaType(): SchemaDefinitionNode {
159 return {
160 kind: Kind.SCHEMA_DEFINITION,
161 directives: [],
162 operationTypes: [
163 getOperationTypeDefinition('query', getNamedType('Query')),
164 getOperationTypeDefinition('mutation', getNamedType('Mutation')),
165 getOperationTypeDefinition('subscription', getNamedType('Subscription')),
166 ],
167 };
168 }
169
170 /**
171 * Generates the basic mutation operations, given the provided table contexts. This will
172 * create a create, delete, and update operation for each table.
173 *
174 * @param types the table contexts from which the mutations are to be generated.
175 * @returns the type definition for mutations, including a create, delete, and update for each table.
176 */
177 private getMutations(types: TableContext[]): ObjectTypeDefinitionNode {
178 const fields = [];
179 for (const typeContext of types) {
180 const type = typeContext.tableTypeDefinition;
181 const formattedTypeValue = toUpper(type.name.value);
182 fields.push(
183 getOperationFieldDefinition(
184 `delete${formattedTypeValue}`,
185 [getInputValueDefinition(getNonNullType(getNamedType(typeContext.tableKeyFieldType)), typeContext.tableKeyField)],
186 getNamedType(`${type.name.value}`),
187 null
188 )
189 );
190 fields.push(
191 getOperationFieldDefinition(
192 `create${formattedTypeValue}`,
193 [getInputValueDefinition(getNonNullType(getNamedType(`Create${formattedTypeValue}Input`)), `create${formattedTypeValue}Input`)],
194 getNamedType(`${type.name.value}`),
195 null
196 )
197 );
198 fields.push(
199 getOperationFieldDefinition(
200 `update${formattedTypeValue}`,
201 [getInputValueDefinition(getNonNullType(getNamedType(`Update${formattedTypeValue}Input`)), `update${formattedTypeValue}Input`)],
202 getNamedType(`${type.name.value}`),
203 null
204 )
205 );
206 }
207 return getTypeDefinition(fields, 'Mutation');
208 }
209
210 /**
211 * Generates the basic subscription operations, given the provided table contexts. This will
212 * create an onCreate subscription for each table.
213 *
214 * @param types the table contexts from which the subscriptions are to be generated.
215 * @returns the type definition for subscriptions, including an onCreate for each table.
216 */
217 private getSubscriptions(types: TableContext[]): ObjectTypeDefinitionNode {
218 const fields = [];
219 for (const typeContext of types) {
220 const type = typeContext.tableTypeDefinition;
221 const formattedTypeValue = toUpper(type.name.value);
222 fields.push(
223 getOperationFieldDefinition(`onCreate${formattedTypeValue}`, [], getNamedType(`${type.name.value}`), [
224 getDirectiveNode(`create${formattedTypeValue}`),
225 ])
226 );
227 }
228 return getTypeDefinition(fields, 'Subscription');
229 }
230
231 /**
232 * Generates the basic query operations, given the provided table contexts. This will
233 * create a get and list operation for each table.
234 *
235 * @param types the table contexts from which the queries are to be generated.
236 * @returns the type definition for queries, including a get and list for each table.
237 */
238 private getQueries(types: TableContext[]): ObjectTypeDefinitionNode {
239 const fields = [];
240 for (const typeContext of types) {
241 const type = typeContext.tableTypeDefinition;
242 const formattedTypeValue = toUpper(type.name.value);
243 fields.push(
244 getOperationFieldDefinition(
245 `get${formattedTypeValue}`,
246 [getInputValueDefinition(getNonNullType(getNamedType(typeContext.tableKeyFieldType)), typeContext.tableKeyField)],
247 getNamedType(`${type.name.value}`),
248 null
249 )
250 );
251 fields.push(getOperationFieldDefinition(`list${formattedTypeValue}s`, [], getNamedType(`[${type.name.value}]`), null));
252 }
253 return getTypeDefinition(fields, 'Query');
254 }
255
256 /**
257 * Creates a GraphQL connection type for a given GraphQL type, corresponding to a SQL table name.
258 *
259 * @param tableName the name of the SQL table (and GraphQL type).
260 * @returns a type definition node defining the connection type for the provided type name.
261 */
262 getConnectionType(tableName: string): ObjectTypeDefinitionNode {
263 return getTypeDefinition(
264 [getFieldDefinition('items', getNamedType(`[${tableName}]`)), getFieldDefinition('nextToken', getNamedType('String'))],
265 `${tableName}Connection`
266 );
267 }
268}