UNPKG

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