feat(api): add AppSync GraphQL APIs to the cloud explorer
CI / checks (pull_request) Successful in 28m38s

Create GraphQL APIs with API-key (the first key is made for you), IAM or
Cognito user pool authorization, then edit the schema (a starter schema is
offered, and a rejected one is explained), create and delete API keys, create
data sources for a DynamoDB table, a Lambda function or none, and attach
JavaScript resolvers to fields of the schema from a template for the data
source.

TheCloud's endpoint checks queries against the schema and the API key but does
not run resolvers yet, so the page says so and offers no query runner.
This commit is contained in:
Zakaria
2026-10-25 18:21:46 -04:00
parent 5eee0b7e9b
commit ca3e67a803
23 changed files with 1509 additions and 1 deletions
@@ -0,0 +1,83 @@
import type {CloudProvider, FieldSchema, ServiceSchema, TableColumnSchema} from './types'
/** CreateGraphqlApi's name characters; the console keeps names to a readable length. */
export const GRAPHQL_API_NAME_PATTERN = '^[A-Za-z0-9_\\- ]{1,128}$'
/** CreateDataSource's name rule: a GraphQL-style identifier. */
export const GRAPHQL_DATA_SOURCE_NAME_PATTERN = '^[_A-Za-z][_0-9A-Za-z]{0,64}$'
export const GRAPHQL_AUTH_TYPES = ['API_KEY', 'AWS_IAM', 'AMAZON_COGNITO_USER_POOLS'] as const
export const GRAPHQL_DATA_SOURCE_TYPES = ['NONE', 'AMAZON_DYNAMODB', 'AWS_LAMBDA'] as const
/** AppSync's bounds on an API key's lifetime. */
export const GRAPHQL_API_KEY_MAX_DAYS = 365
const apiColumns: TableColumnSchema[] = [
{name: 'name', label: 'Name'},
{name: 'id', label: 'API ID', path: 'metadata.apiId'},
{name: 'auth', label: 'Authorization', path: 'metadata.authenticationLabel'},
{name: 'endpoint', label: 'GraphQL endpoint', path: 'metadata.graphqlUrl', emptyText: '—'},
]
const apiFilters: FieldSchema[] = [{name: 'search', label: 'Search', type: 'text', required: false}]
export function awsGraphqlSchema(): ServiceSchema {
return {
cloud: 'aws',
service: 'graphql',
displayName: 'AppSync GraphQL APIs',
fields: [
{
name: 'name',
label: 'API name',
type: 'text',
required: true,
validation: {pattern: GRAPHQL_API_NAME_PATTERN, message: 'Use up to 128 letters, numbers, spaces, dashes and underscores'},
},
{
name: 'authenticationType',
label: 'Authorization',
type: 'select',
required: false,
defaultValue: 'API_KEY',
description: 'How clients prove who they are. With an API key, AppSync creates the first key for you.',
options: [
{label: 'API key', value: 'API_KEY'},
{label: 'IAM (signed requests)', value: 'AWS_IAM'},
{label: 'Cognito user pool', value: 'AMAZON_COGNITO_USER_POOLS'},
],
},
{
name: 'userPoolId',
label: 'User pool ID',
type: 'text',
required: false,
requiredWhen: {field: 'authenticationType', equals: 'AMAZON_COGNITO_USER_POOLS'},
visibleWhen: {field: 'authenticationType', values: ['AMAZON_COGNITO_USER_POOLS']},
description: 'The Cognito user pool whose ID tokens the API accepts, such as us-east-1_AbC123.',
validation: {pattern: '^[\\w-]+_[0-9a-zA-Z]+$', message: 'Use a user pool ID such as us-east-1_AbC123'},
},
],
actions: ['list', 'create', 'delete', 'inspect'],
capabilities: {
resourceActions: [
{name: 'list', label: 'List GraphQL APIs', enabled: true, status: 'available', runtimeRequired: true},
{name: 'create', label: 'Create GraphQL API', enabled: true, status: 'available', runtimeRequired: true},
{name: 'delete', label: 'Delete GraphQL API', enabled: true, status: 'available', runtimeRequired: true},
{name: 'inspect', label: 'Inspect GraphQL API', enabled: true, status: 'available', runtimeRequired: true},
],
graphqlActions: [
{name: 'updateSchema', label: 'Save schema', enabled: true, status: 'available', runtimeRequired: true},
{name: 'createApiKey', label: 'Create API key', enabled: true, status: 'available', runtimeRequired: true},
{name: 'deleteApiKey', label: 'Delete API key', enabled: true, status: 'available', runtimeRequired: true},
{name: 'createDataSource', label: 'Create data source', enabled: true, status: 'available', runtimeRequired: true},
{name: 'deleteDataSource', label: 'Delete data source', enabled: true, status: 'available', runtimeRequired: true},
{name: 'createResolver', label: 'Attach resolver', enabled: true, status: 'available', runtimeRequired: true},
{name: 'deleteResolver', label: 'Detach resolver', enabled: true, status: 'available', runtimeRequired: true},
],
},
filters: apiFilters,
columns: apiColumns,
}
}
export function graphqlSchemaFor(cloud: CloudProvider): ServiceSchema | null {
return cloud === 'aws' ? awsGraphqlSchema() : null
}
@@ -238,6 +238,13 @@ export const SERVICE_CATALOG = {
},
identity: {displayName: 'Identity', iconKey: 'iam', group: 'Security', order: 5},
apigateway: {displayName: 'API Gateway', iconKey: 'apigateway', group: 'Integration', order: 10},
graphql: {
displayName: 'GraphQL APIs',
displayNameByCloud: {aws: 'AppSync'},
iconKey: 'graphql',
group: 'Integration',
order: 12,
},
email: {
displayName: 'Email',
displayNameByCloud: {aws: 'SES Mailbox'},
+35
View File
@@ -188,6 +188,7 @@ export type UserPoolActionName =
export type DistributionActionName = 'setEnabled' | 'createInvalidation'
export type AutoScalingActionName = 'setCapacity' | 'terminateInstance'
export type CertificateActionName = 'download' | 'export' | 'revoke'
export type GraphqlActionName = 'updateSchema' | 'createApiKey' | 'deleteApiKey' | 'createDataSource' | 'deleteDataSource' | 'createResolver' | 'deleteResolver'
export type QueryActionName = 'run' | 'results' | 'stop' | 'history' | 'catalog'
export type DataCatalogActionName = 'createTable' | 'deleteTable'
export type FileSystemActionName = 'createMountTarget' | 'deleteMountTarget' | 'createAccessPoint' | 'deleteAccessPoint' | 'attachFunction' | 'detachFunction'
@@ -255,6 +256,7 @@ export interface ServiceSchema {
fileSystemActions?: CapabilitySchema<FileSystemActionName>[]
dataCatalogActions?: CapabilitySchema<DataCatalogActionName>[]
queryActions?: CapabilitySchema<QueryActionName>[]
graphqlActions?: CapabilitySchema<GraphqlActionName>[]
monitoringActions?: CapabilitySchema<MonitoringActionName>[]
/** API Gateway v2 (HTTP and WebSocket APIs): what the routes, stages and security panel can do. */
httpApiActions?: CapabilitySchema<HttpApiActionName>[]
@@ -562,6 +564,32 @@ export interface AccessPointInput {
permissions: string
}
export interface GraphqlApiKeyInput {
description?: string
/** Days until the key expires: 1 to 365, 7 when left out, as in the AppSync console. */
expiresInDays?: number
}
export interface GraphqlDataSourceInput {
name: string
description?: string
type: 'NONE' | 'AMAZON_DYNAMODB' | 'AWS_LAMBDA'
/** AMAZON_DYNAMODB: the table the resolvers read and write. */
tableName?: string
/** AWS_LAMBDA: the function the resolvers invoke. */
functionArn?: string
/** The role AppSync assumes to reach the table or function; AWS requires one for both. */
serviceRoleArn?: string
}
export interface GraphqlResolverInput {
typeName: string
fieldName: string
dataSourceName: string
/** An APPSYNC_JS resolver: request() and response() exported from one module. */
code: string
}
export interface QueryInput {
sql: string
/** The Glue database unqualified table names resolve in. */
@@ -1294,6 +1322,13 @@ export interface CloudServiceAdapter {
/** With `replace`, the group launches a new instance in its place; otherwise desired capacity drops by one. */
terminateAutoScalingInstance?(groupName: string, instanceId: string, replace: boolean): Promise<void>
getCertificatePem?(certificateId: string): Promise<CertificatePem>
updateGraphqlSchema?(apiId: string, definition: string): Promise<CloudResource>
createGraphqlApiKey?(apiId: string, input: GraphqlApiKeyInput): Promise<CloudResource>
deleteGraphqlApiKey?(apiId: string, keyId: string): Promise<CloudResource>
createGraphqlDataSource?(apiId: string, input: GraphqlDataSourceInput): Promise<CloudResource>
deleteGraphqlDataSource?(apiId: string, name: string): Promise<CloudResource>
createGraphqlResolver?(apiId: string, input: GraphqlResolverInput): Promise<CloudResource>
deleteGraphqlResolver?(apiId: string, typeName: string, fieldName: string): Promise<CloudResource>
startQuery?(workGroup: string, input: QueryInput): Promise<QueryExecutionSummary>
/** The query's state, and its first rows once it has succeeded. */
getQueryResult?(workGroup: string, queryId: string): Promise<QueryResult>