UNPKG

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