1 | import TemplateContext, { TableContext } from "./RelationalDBSchemaTransformer";
|
2 | import { getNamedType, getNonNullType, getInputValueDefinition, getGraphQLTypeFromMySQLType,
|
3 | getTypeDefinition, getFieldDefinition, getInputTypeDefinition } from './RelationalDBSchemaTransformerUtils'
|
4 | import { AuroraDataAPIClient } from "./AuroraDataAPIClient";
|
5 | import { IRelationalDBReader } from "./IRelationalDBReader";
|
6 |
|
7 | /**
|
8 | * A class to manage interactions with a Aurora Serverless MySQL Relational Databse
|
9 | * using the Aurora Data API
|
10 | */
|
11 | export class AuroraServerlessMySQLDatabaseReader implements IRelationalDBReader {
|
12 |
|
13 | auroraClient: AuroraDataAPIClient
|
14 | dbRegion: string
|
15 | awsSecretStoreArn: string
|
16 | dbClusterOrInstanceArn: string
|
17 | database: string
|
18 |
|
19 | setAuroraClient(auroraClient: AuroraDataAPIClient) {
|
20 | this.auroraClient = auroraClient
|
21 | }
|
22 |
|
23 | constructor(dbRegion: string, awsSecretStoreArn: string, dbClusterOrInstanceArn: string, database: string, aws:any) {
|
24 | this.auroraClient = new AuroraDataAPIClient(dbRegion, awsSecretStoreArn,
|
25 | dbClusterOrInstanceArn, database, aws)
|
26 | this.dbRegion = dbRegion
|
27 | this.awsSecretStoreArn = awsSecretStoreArn
|
28 | this.dbClusterOrInstanceArn = dbClusterOrInstanceArn
|
29 | this.database = database
|
30 | }
|
31 |
|
32 | /**
|
33 | * Stores some of the Aurora Serverless MySQL context into the template context,
|
34 | * for later consumption.
|
35 | *
|
36 | * @param contextShell the basic template context, with db source independent fields set.
|
37 | * @returns a fully hydrated template context, complete with Aurora Serverless MySQL context.
|
38 | */
|
39 | hydrateTemplateContext = async(contextShell: TemplateContext): Promise<TemplateContext> => {
|
40 |
|
41 | /**
|
42 | * Information needed for creating the AppSync - RDS Data Source
|
43 | * Store as part of the TemplateContext
|
44 | */
|
45 | contextShell.secretStoreArn = this.awsSecretStoreArn
|
46 | contextShell.rdsClusterIdentifier = this.dbClusterOrInstanceArn
|
47 | contextShell.databaseSchema = 'mysql'
|
48 | contextShell.databaseName = this.database
|
49 | contextShell.region = this.dbRegion
|
50 | return contextShell
|
51 | }
|
52 |
|
53 | /**
|
54 | * Gets a list of all the table names in the provided database.
|
55 | *
|
56 | * @returns a list of tablenames inside the database.
|
57 | */
|
58 | listTables = async (): Promise<string[]> => {
|
59 | const results = await this.auroraClient.listTables()
|
60 | return results
|
61 | }
|
62 |
|
63 | /**
|
64 | * Looks up any foreign key constraints that might exist for the provided table.
|
65 | * This is done to ensure our generated schema includes nested types, where possible.
|
66 | *
|
67 | * @param tableName the name of the table to be checked for foreign key constraints.
|
68 | * @returns a list of table names that are applicable as having constraints.
|
69 | */
|
70 | getTableForeignKeyReferences = async (tableName: string) : Promise<string[]> => {
|
71 | const results = await this.auroraClient.getTableForeignKeyReferences(tableName)
|
72 | return results
|
73 | }
|
74 |
|
75 | /**
|
76 | * For the provided table, this will create a table context. That context holds definitions for
|
77 | * the base table type, the create input type, and the update input type (e.g. Post, CreatePostInput, and UpdatePostInput, respectively),
|
78 | * as well as the table primary key structure for proper operation definition.
|
79 | *
|
80 | * Create inputs will only differ from the base table type in that any nested types will not be present. Update table
|
81 | * inputs will differ in that the only required field will be the primary key/identifier, as all fields don't have to
|
82 | * be updated. Instead, it assumes the proper ones were provided on create.
|
83 | *
|
84 | * @param tableName the name of the table to be translated into a GraphQL type.
|
85 | * @returns a promise of a table context structure.
|
86 | */
|
87 | describeTable = async (tableName: string): Promise<TableContext> => {
|
88 | const columnDescriptions = await this.auroraClient.describeTable(tableName)
|
89 | // Fields in the general type (e.g. Post). Both the identifying field and any others the db dictates will be required.
|
90 | const fields = new Array()
|
91 | // Fields in the update input type (e.g. UpdatePostInput). Only the identifying field will be required, any others will be optional.
|
92 | const updateFields = new Array()
|
93 | // Field in the create input type (e.g. CreatePostInput).
|
94 | const createFields = new Array()
|
95 |
|
96 | // The primary key, used to help generate queries and mutations
|
97 | let primaryKey = ""
|
98 | let primaryKeyType = ""
|
99 |
|
100 | // Field Lists needed as context for auto-generating the Query Resolvers
|
101 | const intFieldList = new Array()
|
102 | const stringFieldList = new Array()
|
103 |
|
104 | for (const columnDescription of columnDescriptions) {
|
105 | // If a field is the primary key, save it.
|
106 | if (columnDescription.Key == 'PRI') {
|
107 | primaryKey = columnDescription.Field
|
108 | primaryKeyType = getGraphQLTypeFromMySQLType(columnDescription.Type)
|
109 | } else {
|
110 | /**
|
111 | * If the field is not a key, then store it in the fields list.
|
112 | * As we need this information later to generate query resolvers
|
113 | *
|
114 | * Currently we will only auto-gen query resolvers for the Int and String scalars
|
115 | */
|
116 | const type = getGraphQLTypeFromMySQLType(columnDescription.Type)
|
117 | if (type === 'Int') {
|
118 | intFieldList.push(columnDescription.Field)
|
119 | } else if (type === 'String') {
|
120 | stringFieldList.push(columnDescription.Field)
|
121 | }
|
122 | }
|
123 |
|
124 | // Create the basic field type shape, to be consumed by every field definition
|
125 | const baseType = getNamedType(getGraphQLTypeFromMySQLType(columnDescription.Type))
|
126 |
|
127 | const isPrimaryKey = columnDescription.Key == 'PRI'
|
128 | const isNullable = columnDescription.Null == 'YES'
|
129 |
|
130 | // Generate the field for the general type and the create input type
|
131 | const type = (!isPrimaryKey && isNullable) ? baseType : getNonNullType(baseType)
|
132 | fields.push(getFieldDefinition(columnDescription.Field, type))
|
133 |
|
134 | createFields.push(getInputValueDefinition(type, columnDescription.Field))
|
135 |
|
136 | // Update<type>Input has only the primary key as required, ignoring all other that the database requests as non-nullable
|
137 | const updateType = !isPrimaryKey ? baseType : getNonNullType(baseType)
|
138 | updateFields.push(getInputValueDefinition(updateType, columnDescription.Field))
|
139 | }
|
140 |
|
141 | // Add foreign key for this table
|
142 |
|
143 | // NOTE from @mikeparisstuff: It would be great to re-enable this such that foreign key relationships are
|
144 | // resolver automatically. This code was breaking compilation because it was not
|
145 | // creating XConnection types correctly. This package also does not yet support
|
146 | // wiring up the resolvers (or ideally selection set introspection & automatic JOINs)
|
147 | // so there is not point in creating these connection fields anyway. Disabling until
|
148 | // supported.
|
149 | // let tablesWithRef = await this.getTableForeignKeyReferences(tableName)
|
150 | // for (const tableWithRef of tablesWithRef) {
|
151 | // if (tableWithRef && tableWithRef.length > 0) {
|
152 | // const baseType = getNamedType(`${tableWithRef}Connection`)
|
153 | // fields.push(getFieldDefinition(`${tableWithRef}`, baseType))
|
154 | // }
|
155 | // }
|
156 |
|
157 | return new TableContext(getTypeDefinition(fields, tableName), getInputTypeDefinition(createFields, `Create${tableName}Input`),
|
158 | getInputTypeDefinition(updateFields, `Update${tableName}Input`), primaryKey, primaryKeyType, stringFieldList, intFieldList)
|
159 | }
|
160 | } |
\ | No newline at end of file |