From 942606046769761f0f26de4000623e25c0b47891 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 26 Apr 2023 16:26:27 +0300 Subject: [PATCH 01/30] apiId --- doc/general-config.md | 31 ++ .../__snapshots__/existingAPI.test.ts.snap | 288 ++++++++++++++++++ src/__tests__/api.test.ts | 52 ++++ src/__tests__/schema.test.ts | 11 + src/__tests__/waf.test.ts | 14 + src/getAppSyncConfig.ts | 4 +- src/index.ts | 27 +- src/resources/Api.ts | 113 +++++-- src/types/cloudFormation.ts | 6 +- src/types/plugin.ts | 5 +- src/validation.ts | 3 +- 11 files changed, 515 insertions(+), 39 deletions(-) create mode 100644 src/__tests__/__snapshots__/existingAPI.test.ts.snap diff --git a/doc/general-config.md b/doc/general-config.md index 84dee062..4104afa3 100644 --- a/doc/general-config.md +++ b/doc/general-config.md @@ -50,6 +50,7 @@ appSync: - `logging`: See [Logging](#Logging) - `xrayEnabled`: Boolean. Enable or disable X-Ray tracing. - `tags`: A key-value pair for tagging this AppSync API +- `apiId`: See [ApiId](#ApiId) ## Schema @@ -185,3 +186,33 @@ appSync: - `excludeVerboseContent`: Boolean, Optional. Exclude or not verbose content (headers, response headers, context, and evaluated mapping templates), regardless of field logging level. Defaults to `false`. - `retentionInDays`: Optional. Number of days to retain the logs. Defaults to [`provider.logRetentionInDays`](https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml#general-function-settings). - `roleArn`: Optional. The role ARN to use for AppSync to write into CloudWatch. If not specified, a new role is created by default. + + +## ApiId +If you want to manage your existing AppSync Api through the serverless, you can specify `apiId.` +This is handy if you +- defined your API in the AWS console +- defined your API through the cloudformation in the current or another stack + +To point your resources into existing AppSync API, you must provide apiId, which can be a string or imported value from another stack. +```yaml +appSync: + name: my-api + apiId: "existing api id" +``` + +The following configuration options are only associated with the creation of a new AppSync endpoint and will be ignored if you provide the apiId parameter: +- name +- authentication +- additionalAuthentications +- schema +- domain +- apiKeys +- xrayEnabled +- logging +- waf +- Tags +> Note: you should never specify this parameter if you're managing your AppSync through this plugin since it results in removing your API. + +### Schema +After specifying this parameter, you need to manually keep your schema up to date or from the main stack where your root AppSync API is defined. The plugin is not taking into account schema property due to AppSync limitation and inability to merge schemas across multiple stacks diff --git a/src/__tests__/__snapshots__/existingAPI.test.ts.snap b/src/__tests__/__snapshots__/existingAPI.test.ts.snap new file mode 100644 index 00000000..c9c4e3fb --- /dev/null +++ b/src/__tests__/__snapshots__/existingAPI.test.ts.snap @@ -0,0 +1,288 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Domains should generate domain resources 1`] = ` +Object { + "GraphQlDomainAssociation": Object { + "DeletionPolicy": "Delete", + "DependsOn": Array [ + "GraphQlDomainName", + ], + "Properties": Object { + "ApiId": "123", + "DomainName": "api.example.com", + }, + "Type": "AWS::AppSync::DomainNameApiAssociation", + }, + "GraphQlDomainCertificate": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "DomainName": "api.example.com", + "DomainValidationOptions": Array [ + Object { + "DomainName": "api.example.com", + "HostedZoneId": "Z111111QQQQQQQ", + }, + ], + "ValidationMethod": "DNS", + }, + "Type": "AWS::CertificateManager::Certificate", + }, + "GraphQlDomainName": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "CertificateArn": Object { + "Ref": "GraphQlDomainCertificate", + }, + "DomainName": "api.example.com", + }, + "Type": "AWS::AppSync::DomainName", + }, + "GraphQlDomainRoute53Record": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "AliasTarget": Object { + "DNSName": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "AppSyncDomainName", + ], + }, + "EvaluateTargetHealth": false, + "HostedZoneId": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "HostedZoneId", + ], + }, + }, + "HostedZoneId": "Z111111QQQQQQQ", + "Name": "api.example.com", + "Type": "A", + }, + "Type": "AWS::Route53::RecordSet", + }, +} +`; + +exports[`Domains should generate domain resources with custom certificate ARN 1`] = ` +Object { + "GraphQlDomainAssociation": Object { + "DeletionPolicy": "Delete", + "DependsOn": Array [ + "GraphQlDomainName", + ], + "Properties": Object { + "ApiId": "123", + "DomainName": "api.example.com", + }, + "Type": "AWS::AppSync::DomainNameApiAssociation", + }, + "GraphQlDomainName": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "CertificateArn": "arn:aws:acm:us-east-1:1234567890:certificate/e4b6e9be-1aa7-458d-880e-069622e5be52", + "DomainName": "api.example.com", + }, + "Type": "AWS::AppSync::DomainName", + }, + "GraphQlDomainRoute53Record": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "AliasTarget": Object { + "DNSName": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "AppSyncDomainName", + ], + }, + "EvaluateTargetHealth": false, + "HostedZoneId": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "HostedZoneId", + ], + }, + }, + "HostedZoneName": "example.com.", + "Name": "api.example.com", + "Type": "A", + }, + "Type": "AWS::Route53::RecordSet", + }, +} +`; + +exports[`Domains should generate domain resources with custom hostedZoneId 1`] = ` +Object { + "GraphQlDomainAssociation": Object { + "DeletionPolicy": "Delete", + "DependsOn": Array [ + "GraphQlDomainName", + ], + "Properties": Object { + "ApiId": "123", + "DomainName": "api.example.com", + }, + "Type": "AWS::AppSync::DomainNameApiAssociation", + }, + "GraphQlDomainName": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "CertificateArn": "arn:aws:acm:us-east-1:1234567890:certificate/e4b6e9be-1aa7-458d-880e-069622e5be52", + "DomainName": "api.example.com", + }, + "Type": "AWS::AppSync::DomainName", + }, + "GraphQlDomainRoute53Record": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "AliasTarget": Object { + "DNSName": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "AppSyncDomainName", + ], + }, + "EvaluateTargetHealth": false, + "HostedZoneId": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "HostedZoneId", + ], + }, + }, + "HostedZoneId": "Z111111QQQQQQQ", + "Name": "api.example.com", + "Type": "A", + }, + "Type": "AWS::Route53::RecordSet", + }, +} +`; + +exports[`Domains should generate domain resources with custom hostedZoneName 1`] = ` +Object { + "GraphQlDomainAssociation": Object { + "DeletionPolicy": "Delete", + "DependsOn": Array [ + "GraphQlDomainName", + ], + "Properties": Object { + "ApiId": "123", + "DomainName": "foo.api.example.com", + }, + "Type": "AWS::AppSync::DomainNameApiAssociation", + }, + "GraphQlDomainName": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "CertificateArn": "arn:aws:acm:us-east-1:1234567890:certificate/e4b6e9be-1aa7-458d-880e-069622e5be52", + "DomainName": "foo.api.example.com", + }, + "Type": "AWS::AppSync::DomainName", + }, + "GraphQlDomainRoute53Record": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "AliasTarget": Object { + "DNSName": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "AppSyncDomainName", + ], + }, + "EvaluateTargetHealth": false, + "HostedZoneId": Object { + "Fn::GetAtt": Array [ + "GraphQlDomainName", + "HostedZoneId", + ], + }, + }, + "HostedZoneName": "example.com.", + "Name": "foo.api.example.com", + "Type": "A", + }, + "Type": "AWS::Route53::RecordSet", + }, +} +`; + +exports[`Existing Api compileEndpoint should compile the Api Resource with embedded additional authorizer Lambda function 1`] = ` +Object { + "GraphQlApi": Object { + "Properties": Object { + "AdditionalAuthenticationProviders": Array [ + Object { + "AuthenticationType": "AWS_LAMBDA", + "LambdaAuthorizerConfig": Object { + "AuthorizerResultTtlInSeconds": undefined, + "AuthorizerUri": Object { + "Fn::GetAtt": Array [ + "MyApiAuthorizerLambdaFunction", + "Arn", + ], + }, + "IdentityValidationExpression": undefined, + }, + }, + ], + "AuthenticationType": "API_KEY", + "Name": "MyApi", + "Tags": Array [ + Object { + "Key": "stage", + "Value": "Dev", + }, + ], + "XrayEnabled": false, + }, + "Type": "AWS::AppSync::GraphQLApi", + }, +} +`; + +exports[`Existing Api compileEndpoint should compile the Api Resource with embedded additional authorizer Lambda function 2`] = ` +Object { + "MyApiAuthorizer": Object { + "handler": "index.handler", + }, +} +`; + +exports[`Existing Api compileEndpoint should compile the Api Resource with embedded authorizer Lambda function 1`] = ` +Object { + "GraphQlApi": Object { + "Properties": Object { + "AuthenticationType": "AWS_LAMBDA", + "LambdaAuthorizerConfig": Object { + "AuthorizerResultTtlInSeconds": undefined, + "AuthorizerUri": Object { + "Fn::GetAtt": Array [ + "MyApiAuthorizerLambdaFunction", + "Arn", + ], + }, + "IdentityValidationExpression": undefined, + }, + "Name": "MyApi", + "Tags": Array [ + Object { + "Key": "stage", + "Value": "Dev", + }, + ], + "XrayEnabled": false, + }, + "Type": "AWS::AppSync::GraphQLApi", + }, +} +`; + +exports[`Existing Api compileEndpoint should compile the Api Resource with embedded authorizer Lambda function 2`] = ` +Object { + "MyApiAuthorizer": Object { + "handler": "index.handler", + }, +} +`; diff --git a/src/__tests__/api.test.ts b/src/__tests__/api.test.ts index 9f8c19b6..e64f230c 100644 --- a/src/__tests__/api.test.ts +++ b/src/__tests__/api.test.ts @@ -213,6 +213,10 @@ describe('Api', () => { expect(api.compileEndpoint()).toMatchSnapshot(); expect(api.functions).toMatchSnapshot(); }); + it('should not compile the Api Resource when apiId is provided', () => { + const api = new Api(given.appSyncConfig({ apiId: '123' }), plugin); + expect(api.compileEndpoint()).toMatchInlineSnapshot(`Object {}`); + }); }); describe('Logs', () => { @@ -239,6 +243,13 @@ describe('Api', () => { ); }); + it('should not compile CloudWatch Resources when apiId is provided', () => { + const api = new Api(given.appSyncConfig({ apiId: '1234' }), plugin); + expect(api.compileCloudWatchLogGroup()).toMatchInlineSnapshot( + `Object {}`, + ); + }); + it('should compile CloudWatch Resources when enaabled', () => { const api = new Api( given.appSyncConfig({ @@ -465,6 +476,20 @@ describe('Api', () => { } `); }); + it('should not generate an api key resources when apiId is provided', () => { + const api = new Api( + given.appSyncConfig({ + apiId: '1234', + }), + plugin, + ); + expect( + api.compileApiKey({ + name: 'Default', + description: 'Default Key', + }), + ).toMatchInlineSnapshot(`Object {}`); + }); }); describe('LambdaAuthorizer', () => { @@ -482,6 +507,18 @@ describe('Api', () => { ); }); + it('should not generate the Lambda Authorizer Resources when apiId is provided', () => { + const api = new Api( + given.appSyncConfig({ + apiId: '123', + }), + plugin, + ); + expect(api.compileLambdaAuthorizerPermission()).toMatchInlineSnapshot( + `Object {}`, + ); + }); + it('should generate the Lambda Authorizer Resources from basic auth', () => { const api = new Api( given.appSyncConfig({ @@ -560,6 +597,11 @@ describe('Caching', () => { expect(api.compileCachingResources()).toEqual({}); }); + it('should not generate Resources when apiId is provided', () => { + const api = new Api(given.appSyncConfig({ apiId: '1234' }), plugin); + expect(api.compileCachingResources()).toEqual({}); + }); + it('should generate Resources with defaults', () => { const api = new Api( given.appSyncConfig({ @@ -723,4 +765,14 @@ describe('Domains', () => { ); expect(api.compileCustomDomain()).toMatchSnapshot(); }); + + it('should not generate domain resources when apiId is provided', () => { + const api = new Api( + given.appSyncConfig({ + apiId: '123', + }), + plugin, + ); + expect(api.compileCustomDomain()).toMatchInlineSnapshot(`Object {}`); + }); }); diff --git a/src/__tests__/schema.test.ts b/src/__tests__/schema.test.ts index deae80fe..78079995 100644 --- a/src/__tests__/schema.test.ts +++ b/src/__tests__/schema.test.ts @@ -51,6 +51,17 @@ describe('schema', () => { `); }); + it('should generate a schema resource if apiId is provided', () => { + const api = new Api( + given.appSyncConfig({ + apiId: '123', + }), + plugin, + ); + + expect(api.compileSchema()).toMatchInlineSnapshot(`Object {}`); + }); + it('should merge the schemas', () => { const api = new Api(given.appSyncConfig(), plugin); const schema = new Schema(api, [ diff --git a/src/__tests__/waf.test.ts b/src/__tests__/waf.test.ts index 9575f66b..14e86be3 100644 --- a/src/__tests__/waf.test.ts +++ b/src/__tests__/waf.test.ts @@ -69,6 +69,20 @@ describe('Waf', () => { }); expect(waf.compile()).toMatchSnapshot(); }); + + it('should not generate waf Resources if api id is provided', () => { + const api = new Api( + given.appSyncConfig({ + waf: { + enabled: false, + name: 'Waf', + rules: [], + }, + }), + plugin, + ); + expect(api.compileWafRules()).toEqual({}); + }); }); describe('Throttle rules', () => { diff --git a/src/getAppSyncConfig.ts b/src/getAppSyncConfig.ts index bdfc2fad..c4260d3b 100644 --- a/src/getAppSyncConfig.ts +++ b/src/getAppSyncConfig.ts @@ -82,6 +82,7 @@ export const getAppSyncConfig = (config: AppSyncConfigInput): AppSyncConfig => { const dataSources: Record = {}; const resolvers: Record = {}; const pipelineFunctions: Record = {}; + const additionalAuthentications = config.additionalAuthentications || []; forEach(flattenMaps(config.dataSources), (ds, name) => { dataSources[name] = { @@ -163,11 +164,10 @@ export const getAppSyncConfig = (config: AppSyncConfigInput): AppSyncConfig => { }; }); - const additionalAuthentications = config.additionalAuthentications || []; let apiKeys: Record | undefined; if ( - config.authentication.type === 'API_KEY' || + config.authentication?.type === 'API_KEY' || additionalAuthentications.some((auth) => auth.type === 'API_KEY') ) { const inputKeys = config.apiKeys || []; diff --git a/src/index.ts b/src/index.ts index 6d93c2ea..9b9696bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,6 +65,7 @@ import { ListCertificatesResponse, } from 'aws-sdk/clients/acm'; import terminalLink from 'terminal-link'; +import { AppSyncConfig } from './types/plugin'; const CONSOLE_BASE_URL = 'https://console.aws.amazon.com'; @@ -86,6 +87,7 @@ class ServerlessAppsyncPlugin { public readonly configurationVariablesSources?: VariablesSourcesDefinition; private api?: Api; private naming?: Naming; + private config?: AppSyncConfig; constructor( public serverless: Serverless, @@ -347,6 +349,11 @@ class ServerlessAppsyncPlugin { async getApiId() { this.loadConfig(); + if (this.config?.apiId) { + return this.config.apiId; + } + + if (!this.naming) { throw new this.serverless.classes.Error( 'Could not find the naming service. This should not happen.', @@ -377,6 +384,10 @@ class ServerlessAppsyncPlugin { async gatherData() { const apiId = await this.getApiId(); + if (typeof apiId !== 'string') { + return; + } + const { graphqlApi } = await this.provider.request< GetGraphqlApiRequest, GetGraphqlApiResponse @@ -410,6 +421,10 @@ class ServerlessAppsyncPlugin { async getIntrospection() { const apiId = await this.getApiId(); + if (typeof apiId !== 'string') { + return; + } + const { schema } = await this.provider.request< GetIntrospectionSchemaRequest, GetIntrospectionSchemaResponse @@ -673,10 +688,16 @@ class ServerlessAppsyncPlugin { } async assocDomain() { - const domain = this.getDomain(); const apiId = await this.getApiId(); + + if (typeof apiId !== 'string') { + return; + } + + const domain = this.getDomain(); const assoc = await this.getApiAssocStatus(domain.name); + if (assoc?.associationStatus !== 'NOT_FOUND' && assoc?.apiId !== apiId) { log.warning( `The domain ${domain.name} is currently associated to another API (${assoc?.apiId})`, @@ -957,9 +978,9 @@ class ServerlessAppsyncPlugin { throw error; } } - const config = getAppSyncConfig(appSync); + this.config = getAppSyncConfig(appSync); this.naming = new Naming(appSync.name); - this.api = new Api(config, this); + this.api = new Api(this.config, this); } validateSchemas() { diff --git a/src/resources/Api.ts b/src/resources/Api.ts index 691d1089..ffab539a 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -25,6 +25,7 @@ import { Resolver } from './Resolver'; import { PipelineFunction } from './PipelineFunction'; import { Schema } from './Schema'; import { Waf } from './Waf'; +import { log } from '@serverless/utils/log'; export class Api { public naming: Naming; @@ -40,6 +41,23 @@ export class Api { compile() { const resources: CfnResources = {}; + if (this.config.apiId) { + log.info(` + Updating an existing Graphql API. + The following configuration options are ignored: + - name + - authentication + - additionalAuthentications + - schema + - domain + - apiKeys + - xrayEnabled + - logging + - waf + - tags + `); + } + merge(resources, this.compileEndpoint()); merge(resources, this.compileSchema()); merge(resources, this.compileCustomDomain()); @@ -47,7 +65,6 @@ export class Api { merge(resources, this.compileLambdaAuthorizerPermission()); merge(resources, this.compileWafRules()); merge(resources, this.compileCachingResources()); - forEach(this.config.apiKeys, (key) => { merge(resources, this.compileApiKey(key)); }); @@ -68,6 +85,9 @@ export class Api { } compileEndpoint(): CfnResources { + if (this.config.apiId) { + return {}; + } const logicalId = this.naming.getApiLogicalId(); const endpointResource: CfnResource = { @@ -78,16 +98,17 @@ export class Api { Tags: this.getTagsConfig(), }, }; - - merge( - endpointResource.Properties, - this.compileAuthenticationProvider(this.config.authentication), - ); + if (this.config.authentication) { + merge( + endpointResource.Properties, + this.compileAuthenticationProvider(this.config.authentication), + ); + } if (this.config.additionalAuthentications.length > 0) { merge(endpointResource.Properties, { AdditionalAuthenticationProviders: - this.config.additionalAuthentications?.map((provider) => + this.config.additionalAuthentications.map((provider) => this.compileAuthenticationProvider(provider, true), ), }); @@ -113,7 +134,11 @@ export class Api { } compileCloudWatchLogGroup(): CfnResources { - if (!this.config.logging || this.config.logging.enabled === false) { + if ( + !this.config.logging || + this.config.logging.enabled === false || + this.config.apiId + ) { return {}; } @@ -183,6 +208,9 @@ export class Api { } compileSchema() { + if (!this.config.schema || this.config.apiId) { + return {}; + } const schema = new Schema(this, this.config.schema); return schema.compile(); } @@ -193,7 +221,8 @@ export class Api { if ( !domain || domain.enabled === false || - domain.useCloudFormation === false + domain.useCloudFormation === false || + this.config.apiId ) { return {}; } @@ -279,6 +308,10 @@ export class Api { } compileLambdaAuthorizerPermission(): CfnResources { + if (!this.config.authentication || this.config.apiId) { + return {}; + } + const lambdaAuth = [ ...this.config.additionalAuthentications, this.config.authentication, @@ -308,6 +341,9 @@ export class Api { } compileApiKey(config: ApiKeyConfig) { + if (this.config.apiId) { + return {}; + } const { name, expiresAt, expiresAfter, description, apiKeyId } = config; const startOfHour = DateTime.now().setZone('UTC').startOf('hour'); @@ -356,26 +392,36 @@ export class Api { } compileCachingResources(): CfnResources { - if (this.config.caching && this.config.caching.enabled !== false) { - const cacheConfig = this.config.caching; - const logicalId = this.naming.getCachingLogicalId(); - - return { - [logicalId]: { - Type: 'AWS::AppSync::ApiCache', - Properties: { - ApiCachingBehavior: cacheConfig.behavior, - ApiId: this.getApiId(), - AtRestEncryptionEnabled: cacheConfig.atRestEncryption || false, - TransitEncryptionEnabled: cacheConfig.transitEncryption || false, - Ttl: cacheConfig.ttl || 3600, - Type: cacheConfig.type || 'T2_SMALL', - }, - }, - }; + if ( + !this.config.caching || + this.config.caching?.enabled === false || + this.config.apiId + ) { + return {}; } + // if ( + // !this.config.caching || + // !this.config.caching?.enabled || + // this.config.apiId + // ) { + // return {}; + // } + const cacheConfig = this.config.caching; + const logicalId = this.naming.getCachingLogicalId(); - return {}; + return { + [logicalId]: { + Type: 'AWS::AppSync::ApiCache', + Properties: { + ApiCachingBehavior: cacheConfig.behavior, + ApiId: this.getApiId(), + AtRestEncryptionEnabled: cacheConfig.atRestEncryption || false, + TransitEncryptionEnabled: cacheConfig.transitEncryption || false, + Ttl: cacheConfig.ttl || 3600, + Type: cacheConfig.type || 'T2_SMALL', + }, + }, + }; } compileDataSource(dsConfig: DataSourceConfig): CfnResources { @@ -396,7 +442,11 @@ export class Api { } compileWafRules() { - if (!this.config.waf || this.config.waf.enabled === false) { + if ( + !this.config.waf || + this.config.waf.enabled === false || + this.config.apiId + ) { return {}; } @@ -405,6 +455,9 @@ export class Api { } getApiId() { + if (this.config.apiId) { + return this.config.apiId; + } const logicalIdGraphQLApi = this.naming.getApiLogicalId(); return { 'Fn::GetAtt': [logicalIdGraphQLApi, 'ApiId'], @@ -525,10 +578,10 @@ export class Api { } hasDataSource(name: string) { - return name in this.config.dataSources; + return name in (this.config.dataSources || {}); } hasPipelineFunction(name: string) { - return name in this.config.pipelineFunctions; + return name in (this.config.pipelineFunctions || {}); } } diff --git a/src/types/cloudFormation.ts b/src/types/cloudFormation.ts index 6100d4e9..7087728f 100644 --- a/src/types/cloudFormation.ts +++ b/src/types/cloudFormation.ts @@ -16,7 +16,11 @@ export type FnSub = { 'Fn::Sub': [string, Record]; }; -export type IntrinsicFunction = FnGetAtt | FnJoin | FnRef | FnSub; +export type FnImportValue = { + 'Fn::ImportValue': string; +}; + +export type IntrinsicFunction = FnGetAtt | FnJoin | FnRef | FnSub | FnImportValue; export type CfnDeltaSyncConfig = { BaseTableTTL: number; diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 84d6debb..dd70cec1 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -2,9 +2,9 @@ import { CfnWafRuleStatement, IntrinsicFunction } from './cloudFormation'; export type AppSyncConfig = { name: string; - schema: string[]; - authentication: Auth; + authentication?: Auth; additionalAuthentications: Auth[]; + schema?: string[]; domain?: DomainConfig; apiKeys?: Record; dataSources: Record; @@ -16,6 +16,7 @@ export type AppSyncConfig = { caching?: CachingConfig; waf?: WafConfig; tags?: Record; + apiId?: string | IntrinsicFunction; }; export type IamStatement = { diff --git a/src/validation.ts b/src/validation.ts index abbf8fbe..8d515907 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -833,8 +833,9 @@ export const appSyncSchema = { ], errorMessage: 'contains invalid pipeline function definitions', }, + apiId: { $ref: '#/definitions/stringOrIntrinsicFunction' }, }, - required: ['name', 'authentication'], + required: ['name'], additionalProperties: { not: true, errorMessage: 'invalid (unknown) property', From 3c5c8f57750009e00f2a9c251a6d9578f9ac3575 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 26 Apr 2023 16:35:48 +0300 Subject: [PATCH 02/30] update snapshots --- .../__snapshots__/existingAPI.test.ts.snap | 288 ------------------ .../__snapshots__/base.test.ts.snap | 1 - 2 files changed, 289 deletions(-) delete mode 100644 src/__tests__/__snapshots__/existingAPI.test.ts.snap diff --git a/src/__tests__/__snapshots__/existingAPI.test.ts.snap b/src/__tests__/__snapshots__/existingAPI.test.ts.snap deleted file mode 100644 index c9c4e3fb..00000000 --- a/src/__tests__/__snapshots__/existingAPI.test.ts.snap +++ /dev/null @@ -1,288 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Domains should generate domain resources 1`] = ` -Object { - "GraphQlDomainAssociation": Object { - "DeletionPolicy": "Delete", - "DependsOn": Array [ - "GraphQlDomainName", - ], - "Properties": Object { - "ApiId": "123", - "DomainName": "api.example.com", - }, - "Type": "AWS::AppSync::DomainNameApiAssociation", - }, - "GraphQlDomainCertificate": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "DomainName": "api.example.com", - "DomainValidationOptions": Array [ - Object { - "DomainName": "api.example.com", - "HostedZoneId": "Z111111QQQQQQQ", - }, - ], - "ValidationMethod": "DNS", - }, - "Type": "AWS::CertificateManager::Certificate", - }, - "GraphQlDomainName": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "CertificateArn": Object { - "Ref": "GraphQlDomainCertificate", - }, - "DomainName": "api.example.com", - }, - "Type": "AWS::AppSync::DomainName", - }, - "GraphQlDomainRoute53Record": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "AliasTarget": Object { - "DNSName": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "AppSyncDomainName", - ], - }, - "EvaluateTargetHealth": false, - "HostedZoneId": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "HostedZoneId", - ], - }, - }, - "HostedZoneId": "Z111111QQQQQQQ", - "Name": "api.example.com", - "Type": "A", - }, - "Type": "AWS::Route53::RecordSet", - }, -} -`; - -exports[`Domains should generate domain resources with custom certificate ARN 1`] = ` -Object { - "GraphQlDomainAssociation": Object { - "DeletionPolicy": "Delete", - "DependsOn": Array [ - "GraphQlDomainName", - ], - "Properties": Object { - "ApiId": "123", - "DomainName": "api.example.com", - }, - "Type": "AWS::AppSync::DomainNameApiAssociation", - }, - "GraphQlDomainName": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "CertificateArn": "arn:aws:acm:us-east-1:1234567890:certificate/e4b6e9be-1aa7-458d-880e-069622e5be52", - "DomainName": "api.example.com", - }, - "Type": "AWS::AppSync::DomainName", - }, - "GraphQlDomainRoute53Record": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "AliasTarget": Object { - "DNSName": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "AppSyncDomainName", - ], - }, - "EvaluateTargetHealth": false, - "HostedZoneId": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "HostedZoneId", - ], - }, - }, - "HostedZoneName": "example.com.", - "Name": "api.example.com", - "Type": "A", - }, - "Type": "AWS::Route53::RecordSet", - }, -} -`; - -exports[`Domains should generate domain resources with custom hostedZoneId 1`] = ` -Object { - "GraphQlDomainAssociation": Object { - "DeletionPolicy": "Delete", - "DependsOn": Array [ - "GraphQlDomainName", - ], - "Properties": Object { - "ApiId": "123", - "DomainName": "api.example.com", - }, - "Type": "AWS::AppSync::DomainNameApiAssociation", - }, - "GraphQlDomainName": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "CertificateArn": "arn:aws:acm:us-east-1:1234567890:certificate/e4b6e9be-1aa7-458d-880e-069622e5be52", - "DomainName": "api.example.com", - }, - "Type": "AWS::AppSync::DomainName", - }, - "GraphQlDomainRoute53Record": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "AliasTarget": Object { - "DNSName": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "AppSyncDomainName", - ], - }, - "EvaluateTargetHealth": false, - "HostedZoneId": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "HostedZoneId", - ], - }, - }, - "HostedZoneId": "Z111111QQQQQQQ", - "Name": "api.example.com", - "Type": "A", - }, - "Type": "AWS::Route53::RecordSet", - }, -} -`; - -exports[`Domains should generate domain resources with custom hostedZoneName 1`] = ` -Object { - "GraphQlDomainAssociation": Object { - "DeletionPolicy": "Delete", - "DependsOn": Array [ - "GraphQlDomainName", - ], - "Properties": Object { - "ApiId": "123", - "DomainName": "foo.api.example.com", - }, - "Type": "AWS::AppSync::DomainNameApiAssociation", - }, - "GraphQlDomainName": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "CertificateArn": "arn:aws:acm:us-east-1:1234567890:certificate/e4b6e9be-1aa7-458d-880e-069622e5be52", - "DomainName": "foo.api.example.com", - }, - "Type": "AWS::AppSync::DomainName", - }, - "GraphQlDomainRoute53Record": Object { - "DeletionPolicy": "Delete", - "Properties": Object { - "AliasTarget": Object { - "DNSName": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "AppSyncDomainName", - ], - }, - "EvaluateTargetHealth": false, - "HostedZoneId": Object { - "Fn::GetAtt": Array [ - "GraphQlDomainName", - "HostedZoneId", - ], - }, - }, - "HostedZoneName": "example.com.", - "Name": "foo.api.example.com", - "Type": "A", - }, - "Type": "AWS::Route53::RecordSet", - }, -} -`; - -exports[`Existing Api compileEndpoint should compile the Api Resource with embedded additional authorizer Lambda function 1`] = ` -Object { - "GraphQlApi": Object { - "Properties": Object { - "AdditionalAuthenticationProviders": Array [ - Object { - "AuthenticationType": "AWS_LAMBDA", - "LambdaAuthorizerConfig": Object { - "AuthorizerResultTtlInSeconds": undefined, - "AuthorizerUri": Object { - "Fn::GetAtt": Array [ - "MyApiAuthorizerLambdaFunction", - "Arn", - ], - }, - "IdentityValidationExpression": undefined, - }, - }, - ], - "AuthenticationType": "API_KEY", - "Name": "MyApi", - "Tags": Array [ - Object { - "Key": "stage", - "Value": "Dev", - }, - ], - "XrayEnabled": false, - }, - "Type": "AWS::AppSync::GraphQLApi", - }, -} -`; - -exports[`Existing Api compileEndpoint should compile the Api Resource with embedded additional authorizer Lambda function 2`] = ` -Object { - "MyApiAuthorizer": Object { - "handler": "index.handler", - }, -} -`; - -exports[`Existing Api compileEndpoint should compile the Api Resource with embedded authorizer Lambda function 1`] = ` -Object { - "GraphQlApi": Object { - "Properties": Object { - "AuthenticationType": "AWS_LAMBDA", - "LambdaAuthorizerConfig": Object { - "AuthorizerResultTtlInSeconds": undefined, - "AuthorizerUri": Object { - "Fn::GetAtt": Array [ - "MyApiAuthorizerLambdaFunction", - "Arn", - ], - }, - "IdentityValidationExpression": undefined, - }, - "Name": "MyApi", - "Tags": Array [ - Object { - "Key": "stage", - "Value": "Dev", - }, - ], - "XrayEnabled": false, - }, - "Type": "AWS::AppSync::GraphQLApi", - }, -} -`; - -exports[`Existing Api compileEndpoint should compile the Api Resource with embedded authorizer Lambda function 2`] = ` -Object { - "MyApiAuthorizer": Object { - "handler": "index.handler", - }, -} -`; diff --git a/src/__tests__/validation/__snapshots__/base.test.ts.snap b/src/__tests__/validation/__snapshots__/base.test.ts.snap index 3406190c..144f890d 100644 --- a/src/__tests__/validation/__snapshots__/base.test.ts.snap +++ b/src/__tests__/validation/__snapshots__/base.test.ts.snap @@ -53,6 +53,5 @@ exports[`Valdiation Waf Invalid should validate a Throttle limit 1`] = ` exports[`Valdiation should validate 1`] = ` ": must have required property 'name' -: must have required property 'authentication' /unknownPorp: invalid (unknown) property" `; From 489896dac24292239b58e9707063d1152ef789fb Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 26 Apr 2023 19:22:59 +0300 Subject: [PATCH 03/30] cleanup --- src/resources/Api.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/resources/Api.ts b/src/resources/Api.ts index ffab539a..3bd3312b 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -399,13 +399,7 @@ export class Api { ) { return {}; } - // if ( - // !this.config.caching || - // !this.config.caching?.enabled || - // this.config.apiId - // ) { - // return {}; - // } + const cacheConfig = this.config.caching; const logicalId = this.naming.getCachingLogicalId(); @@ -444,7 +438,7 @@ export class Api { compileWafRules() { if ( !this.config.waf || - this.config.waf.enabled === false || + this.config.waf?.enabled === false || this.config.apiId ) { return {}; From 5c699d167aaf30f204fee4ea0e262ff07f2cd3e4 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Thu, 27 Apr 2023 17:31:32 +0300 Subject: [PATCH 04/30] drop depends on schema if apiId is provided --- src/resources/Api.ts | 22 +++++++++++++--------- src/resources/Resolver.ts | 4 +++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/resources/Api.ts b/src/resources/Api.ts index 3bd3312b..22a9d4d2 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -41,7 +41,7 @@ export class Api { compile() { const resources: CfnResources = {}; - if (this.config.apiId) { + if (this.isExistingApi()) { log.info(` Updating an existing Graphql API. The following configuration options are ignored: @@ -85,7 +85,7 @@ export class Api { } compileEndpoint(): CfnResources { - if (this.config.apiId) { + if (this.isExistingApi()) { return {}; } const logicalId = this.naming.getApiLogicalId(); @@ -137,7 +137,7 @@ export class Api { if ( !this.config.logging || this.config.logging.enabled === false || - this.config.apiId + this.isExistingApi() ) { return {}; } @@ -208,7 +208,7 @@ export class Api { } compileSchema() { - if (!this.config.schema || this.config.apiId) { + if (!this.config.schema || this.isExistingApi()) { return {}; } const schema = new Schema(this, this.config.schema); @@ -222,7 +222,7 @@ export class Api { !domain || domain.enabled === false || domain.useCloudFormation === false || - this.config.apiId + this.isExistingApi() ) { return {}; } @@ -308,7 +308,7 @@ export class Api { } compileLambdaAuthorizerPermission(): CfnResources { - if (!this.config.authentication || this.config.apiId) { + if (!this.config.authentication || this.isExistingApi()) { return {}; } @@ -341,7 +341,7 @@ export class Api { } compileApiKey(config: ApiKeyConfig) { - if (this.config.apiId) { + if (this.isExistingApi()) { return {}; } const { name, expiresAt, expiresAfter, description, apiKeyId } = config; @@ -395,7 +395,7 @@ export class Api { if ( !this.config.caching || this.config.caching?.enabled === false || - this.config.apiId + this.isExistingApi() ) { return {}; } @@ -439,7 +439,7 @@ export class Api { if ( !this.config.waf || this.config.waf?.enabled === false || - this.config.apiId + this.isExistingApi() ) { return {}; } @@ -458,6 +458,10 @@ export class Api { }; } + isExistingApi() { + return !!this.config?.apiId; + } + getUserPoolConfig(auth: CognitoAuth, isAdditionalAuth = false) { const userPoolConfig = { AwsRegion: auth.config.awsRegion || { 'Fn::Sub': '${AWS::Region}' }, diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 249777d1..77560835 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -125,7 +125,9 @@ export class Resolver { return { [logicalIdResolver]: { Type: 'AWS::AppSync::Resolver', - DependsOn: [logicalIdGraphQLSchema], + ...(!this.api.isExistingApi() && { + DependsOn: [logicalIdGraphQLSchema], + }), Properties, }, }; From 5626a0de4b4c58b8420a84f849b319712747d121 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 15 Nov 2024 14:17:41 +0100 Subject: [PATCH 05/30] fix: missing type import --- src/types/plugin.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 8890def2..d34e7fd3 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -1,5 +1,5 @@ -import { BuildOptions } from 'esbuild'; -import { +import type { BuildOptions } from 'esbuild'; +import type { Auth, DomainConfig, ApiKeyConfig, @@ -17,6 +17,7 @@ import { Substitutions, EnvironmentVariables, } from './common'; +import type { IntrinsicFunction } from './cloudFormation'; export * from './common'; export type AppSyncConfig = { From 440dedff5523208cbf33e24b666c5036e1ee160b Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 15 Nov 2024 16:12:29 +0100 Subject: [PATCH 06/30] WIP --- src/index.ts | 18 ++++++++++++++---- src/resources/Api.ts | 20 ++++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4c42f2b1..59f4838e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -375,7 +375,6 @@ class ServerlessAppsyncPlugin { return this.config.apiId; } - if (!this.naming) { throw new this.serverless.classes.Error( 'Could not find the naming service. This should not happen.', @@ -406,8 +405,16 @@ class ServerlessAppsyncPlugin { async gatherData() { const apiId = await this.getApiId(); - if (typeof apiId !== 'string') { - return; + // TODO : Check if the api key was provided from the config + //! This function should not be run from a child service stacck + if (!apiId) { + throw new this.serverless.classes.Error('Unable to get AppSync Api Id'); + } + if (apiId !== 'string') { + // TODO : Handle IntrinsicFunction ? + throw new this.serverless.classes.Error( + 'AppSync apiId cannot be an IntrinsicFunction', + ); } const { graphqlApi } = await this.provider.request< @@ -443,6 +450,8 @@ class ServerlessAppsyncPlugin { async getIntrospection() { const apiId = await this.getApiId(); + // TODO : Check if the api key was provided from the config + //! This function should not be run from a child service stacck if (typeof apiId !== 'string') { return; } @@ -713,6 +722,8 @@ class ServerlessAppsyncPlugin { async assocDomain() { const apiId = await this.getApiId(); + // TODO : Check if the api key was provided from the config + //! This function should not be run from a child service stacck if (typeof apiId !== 'string') { return; } @@ -720,7 +731,6 @@ class ServerlessAppsyncPlugin { const domain = this.getDomain(); const assoc = await this.getApiAssocStatus(domain.name); - if (assoc?.associationStatus !== 'NOT_FOUND' && assoc?.apiId !== apiId) { this.utils.log.warning( `The domain ${domain.name} is currently associated to another API (${assoc?.apiId})`, diff --git a/src/resources/Api.ts b/src/resources/Api.ts index f29bc153..6a94ae76 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -41,6 +41,7 @@ export class Api { compile() { const resources: CfnResources = {}; + // TODO : Use the validator if (this.isExistingApi()) { log.info(` Updating an existing Graphql API. @@ -99,12 +100,13 @@ export class Api { EnvironmentVariables: this.config.environment, }, }; - if (this.config.authentication) { - merge( - endpointResource.Properties, - this.compileAuthenticationProvider(this.config.authentication), - ); - } + // TODO : Handle the type properly + //! authentication is always required in this context + if (!this.config.authentication) return; + merge( + endpointResource.Properties, + this.compileAuthenticationProvider(this.config.authentication), + ); if (this.config.additionalAuthentications.length > 0) { merge(endpointResource.Properties, { @@ -600,6 +602,7 @@ export class Api { : lambdaArn; } + // TODO : Make those required (remove || {}) hasDataSource(name: string) { return name in (this.config.dataSources || {}); } @@ -607,4 +610,9 @@ export class Api { hasPipelineFunction(name: string) { return name in (this.config.pipelineFunctions || {}); } + + //? I understand why you made those optional, but I'd rather keep them as required. + //? If you look here, those are actually already optional from a config point of view. + //? Then, getAppSyncConfig() makes sure to fill them with empty {} + //? if needed for when it's injected in the compiler. } From 8ba627b851277e687e830709e7c3fd394f7ba672 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 15 Nov 2024 17:22:52 +0100 Subject: [PATCH 07/30] wip --- src/resources/Api.ts | 4 +++ src/types/plugin.ts | 70 +++++++++++++++++++++++++++++++++++++++++++- src/validation.ts | 5 ++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/resources/Api.ts b/src/resources/Api.ts index 92a9663a..f66e3286 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -556,4 +556,8 @@ export class Api { hasPipelineFunction(name: string) { return name in this.config.pipelineFunctions; } + //? I understand why you made those optional, but I'd rather keep them as required. + //? If you look here, those are actually already optional from a config point of view. + //? Then, getAppSyncConfig() makes sure to fill them with empty {} if needed for when it's injected in the compiler. + // https://github.com/sid88in/serverless-appsync-plugin/blob/05164d8847a554d56bb73590fdc35bf0bda5198e/src/getAppSyncConfig.ts#L36-L46 } diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 0b8bf8f0..892239c7 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -9,7 +9,10 @@ import { SyncConfig, DsHttpConfig, DsDynamoDBConfig, - DsRelationalDbConfig, + DsRelationalDbCoas mentioned above, we should have 2 schemas: one for a usual appsync config (main api or single stack) + // and one for "external API" which only accepts supporters fields. + // This way, authentication can remain required in single stack scenarios + // onfig, DsOpenSearchConfig, DsLambdaConfig, DsEventBridgeConfig, @@ -19,6 +22,71 @@ import { } from './common'; export * from './common'; +// TODO: Split into multiple configs +//? If most of the parameters are ignored when using apiId +//? you should define type like this to avoid misusage of the config, +//? it can be nice for those who use this type in their serverless.ts: +/* ts */` +export type BaseAppSyncConfig = { + dataSources: Record; + resolvers: Record; + pipelineFunctions: Record; + substitutions?: Substitutions; + caching?: CachingConfig; +}; + +export type NewAppSyncConfig = BaseAppSyncConfig & { + name: string; + schema: string[]; + authentication: Auth; + additionalAuthentications: Auth[]; + domain?: DomainConfig; + apiKeys?: Record; + xrayEnabled?: boolean; + logging?: LoggingConfig; + waf?: WafConfig; + tags?: Record; +}; + +export type ExistingAppSyncConfig = BaseAppSyncConfig & { + apiId: string | IntrinsicFunction; +}; + +export type AppSyncConfig = NewAppSyncConfig | ExistingAppSyncConfig; +`; + +//? I agree on this and it joins what I commented earlier. +//? The same should happen in the validation json schema. +//? I would use something like a union. +/* ts */` +export type BaseAppSyncConfig = { + dataSources: Record; + resolvers: Record; + pipelineFunctions: Record; + substitutions?: Substitutions; + caching?: CachingConfig; +}; + +export type FullAppSyncConfig = BaseAppSyncConfig & { + name: string; + schema: string[]; + authentication: Auth; + additionalAuthentications: Auth[]; + domain?: DomainConfig; + apiKeys?: Record; + xrayEnabled?: boolean; + logging?: LoggingConfig; + waf?: WafConfig; + tags?: Record; +}; + +export type SharedAppSyncConfig = BaseAppSyncConfig & { + apiId: string; +}; + +export type AppSyncConfig = FullAppSyncConfig | SharedAppSyncConfig +`//! (not tested, might need adjustments) + export type AppSyncConfig = { name: string; schema: string[]; diff --git a/src/validation.ts b/src/validation.ts index 0a961928..a7776506 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -22,6 +22,11 @@ const DATASOURCE_TYPES = [ 'AMAZON_EVENTBRIDGE', ] as const; +// TODO: Split in 2 schemas +//? as mentioned above, we should have 2 schemas: one for a usual appsync config (main api or single stack) +//? and one for "external API" which only accepts supporters fields. +//? This way, authentication can remain required in single stack scenarios +//? or for main stack and be "forbidden" for sub stacks export const appSyncSchema = { type: 'object', definitions: { From 32c9e0797ce883b631168b0288ecd62724243317 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 15 Nov 2024 17:27:14 +0100 Subject: [PATCH 08/30] wip --- src/types/plugin.ts | 103 +++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 9872c7ab..05726e8e 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -9,10 +9,7 @@ import type { SyncConfig, DsHttpConfig, DsDynamoDBConfig, - DsRelationalDbCoas mentioned above, we should have 2 schemas: one for a usual appsync config (main api or single stack) - // and one for "external API" which only accepts supporters fields. - // This way, authentication can remain required in single stack scenarios - // onfig, + DsRelationalDbConfig, DsOpenSearchConfig, DsLambdaConfig, DsEventBridgeConfig, @@ -27,66 +24,66 @@ export * from './common'; //? If most of the parameters are ignored when using apiId //? you should define type like this to avoid misusage of the config, //? it can be nice for those who use this type in their serverless.ts: -/* ts */` -export type BaseAppSyncConfig = { - dataSources: Record; - resolvers: Record; - pipelineFunctions: Record; - substitutions?: Substitutions; - caching?: CachingConfig; -}; +/* ts */ ` + export type BaseAppSyncConfig = { + dataSources: Record; + resolvers: Record; + pipelineFunctions: Record; + substitutions?: Substitutions; + caching?: CachingConfig; + }; -export type NewAppSyncConfig = BaseAppSyncConfig & { - name: string; - schema: string[]; - authentication: Auth; - additionalAuthentications: Auth[]; - domain?: DomainConfig; - apiKeys?: Record; - xrayEnabled?: boolean; - logging?: LoggingConfig; - waf?: WafConfig; - tags?: Record; -}; + export type NewAppSyncConfig = BaseAppSyncConfig & { + name: string; + schema: string[]; + authentication: Auth; + additionalAuthentications: Auth[]; + domain?: DomainConfig; + apiKeys?: Record; + xrayEnabled?: boolean; + logging?: LoggingConfig; + waf?: WafConfig; + tags?: Record; + }; -export type ExistingAppSyncConfig = BaseAppSyncConfig & { - apiId: string | IntrinsicFunction; -}; + export type ExistingAppSyncConfig = BaseAppSyncConfig & { + apiId: string | IntrinsicFunction; + }; -export type AppSyncConfig = NewAppSyncConfig | ExistingAppSyncConfig; + export type AppSyncConfig = NewAppSyncConfig | ExistingAppSyncConfig; `; //? I agree on this and it joins what I commented earlier. //? The same should happen in the validation json schema. //? I would use something like a union. -/* ts */` -export type BaseAppSyncConfig = { - dataSources: Record; - resolvers: Record; - pipelineFunctions: Record; - substitutions?: Substitutions; - caching?: CachingConfig; -}; +/* ts */ ` + export type BaseAppSyncConfig = { + dataSources: Record; + resolvers: Record; + pipelineFunctions: Record; + substitutions?: Substitutions; + caching?: CachingConfig; + }; -export type FullAppSyncConfig = BaseAppSyncConfig & { - name: string; - schema: string[]; - authentication: Auth; - additionalAuthentications: Auth[]; - domain?: DomainConfig; - apiKeys?: Record; - xrayEnabled?: boolean; - logging?: LoggingConfig; - waf?: WafConfig; - tags?: Record; -}; + export type FullAppSyncConfig = BaseAppSyncConfig & { + name: string; + schema: string[]; + authentication: Auth; + additionalAuthentications: Auth[]; + domain?: DomainConfig; + apiKeys?: Record; + xrayEnabled?: boolean; + logging?: LoggingConfig; + waf?: WafConfig; + tags?: Record; + }; -export type SharedAppSyncConfig = BaseAppSyncConfig & { - apiId: string; -}; + export type SharedAppSyncConfig = BaseAppSyncConfig & { + apiId: string; + }; -export type AppSyncConfig = FullAppSyncConfig | SharedAppSyncConfig -`//! (not tested, might need adjustments) + export type AppSyncConfig = FullAppSyncConfig | SharedAppSyncConfig +`; //! (not tested, might need adjustments) export type AppSyncConfig = { name: string; From b4f131dfa1736329d99123cd88b4962b896c7f38 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 07:02:38 +0100 Subject: [PATCH 09/30] Added checks wherethe naming module is used --- src/getAppSyncConfig.ts | 10 ++- src/index.ts | 24 ++++-- src/resources/Api.ts | 126 +++++++++++++----------------- src/resources/DataSource.ts | 1 + src/resources/JsResolver.ts | 1 + src/resources/PipelineFunction.ts | 1 + src/resources/Resolver.ts | 2 + src/resources/Schema.ts | 1 + src/resources/SyncConfig.ts | 1 + src/resources/Waf.ts | 26 ++++-- src/types/plugin.ts | 102 ++++++------------------ 11 files changed, 136 insertions(+), 159 deletions(-) diff --git a/src/getAppSyncConfig.ts b/src/getAppSyncConfig.ts index 82a879fb..c67e1882 100644 --- a/src/getAppSyncConfig.ts +++ b/src/getAppSyncConfig.ts @@ -5,6 +5,9 @@ import { DataSourceConfig, PipelineFunctionConfig, ResolverConfig, + isSharedApiConfig, + SharedAppSyncConfig, + FullAppSyncConfig, } from './types/plugin'; import { forEach, merge } from 'lodash'; @@ -127,7 +130,6 @@ export const getAppSyncConfig = ( }; }); - let apiKeys: Record | undefined; if ( config.authentication?.type === 'API_KEY' || @@ -146,7 +148,7 @@ export const getAppSyncConfig = ( }, {}); } - return { + const appSyncConfig = { ...config, additionalAuthentications, apiKeys, @@ -155,4 +157,8 @@ export const getAppSyncConfig = ( resolvers, pipelineFunctions, }; + + return isSharedApiConfig(appSyncConfig) + ? (appSyncConfig satisfies SharedAppSyncConfig) + : (appSyncConfig satisfies FullAppSyncConfig); }; diff --git a/src/index.ts b/src/index.ts index 59f4838e..9bf5b498 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,7 +64,7 @@ import { ListCertificatesResponse, } from 'aws-sdk/clients/acm'; import terminalLink from 'terminal-link'; -import { AppSyncConfig } from './types/plugin'; +import { AppSyncConfig, isSharedApiConfig } from './types/plugin'; const CONSOLE_BASE_URL = 'https://console.aws.amazon.com'; @@ -370,8 +370,11 @@ class ServerlessAppsyncPlugin { async getApiId() { this.loadConfig(); + if (!this.config) { + throw new this.serverless.classes.Error('Unable to load the config'); + } - if (this.config?.apiId) { + if (isSharedApiConfig(this.config) && this.config?.apiId) { return this.config.apiId; } @@ -580,6 +583,12 @@ class ServerlessAppsyncPlugin { ); } + if (isSharedApiConfig(this.api.config)) { + throw new this.serverless.classes.Error( + 'AppSync configuration not found', + ); + } + const { domain } = this.api.config; if (!domain) { throw new this.serverless.classes.Error('Domain configuration not found'); @@ -966,15 +975,18 @@ class ServerlessAppsyncPlugin { } displayEndpoints() { + if (!this.api?.config || isSharedApiConfig(this.api.config)) { + throw this.serverless.classes.Error( + 'Impossible to display endpoints from a Shared Appsync', + ); + } const endpoints = this.gatheredData.apis.map( ({ type, uri }) => `${type}: ${uri}`, ); - if (endpoints.length === 0) { - return; - } + if (endpoints.length === 0) return; - const { name } = this.api?.config?.domain || {}; + const { name } = this.api.config?.domain || {}; if (name) { endpoints.push(`graphql: https://${name}/graphql`); endpoints.push(`realtime: wss://${name}/graphql/realtime`); diff --git a/src/resources/Api.ts b/src/resources/Api.ts index 57b9680c..f67b4d80 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -16,6 +16,9 @@ import { LambdaConfig, OidcAuth, ResolverConfig, + FullAppSyncConfig, + SharedAppSyncConfig, + isSharedApiConfig, } from '../types/plugin'; import { getHostedZoneName, parseDuration } from '../utils'; import { DateTime, Duration } from 'luxon'; @@ -28,48 +31,36 @@ import { Waf } from './Waf'; import { log } from '@serverless/utils/log'; export class Api { - public naming: Naming; + public naming?: Naming; + public config: AppSyncConfig; public functions: Record> = {}; - constructor( - public config: AppSyncConfig, - public plugin: ServerlessAppsyncPlugin, - ) { - this.naming = new Naming(this.config.name); + constructor(config: AppSyncConfig, public plugin: ServerlessAppsyncPlugin) { + if ('apiId' in config) { + this.config = config satisfies SharedAppSyncConfig; + // Todo: check if naming is required here + } else { + this.config = config satisfies FullAppSyncConfig; + this.naming = new Naming(this.config.name); + } } compile() { const resources: CfnResources = {}; - // TODO : Use the validator - if (this.isExistingApi()) { - log.info(` - Updating an existing Graphql API. - The following configuration options are ignored: - - name - - authentication - - additionalAuthentications - - schema - - domain - - apiKeys - - xrayEnabled - - logging - - waf - - tags - `); + if (!isSharedApiConfig(this.config)) { + merge(resources, this.compileEndpoint()); + merge(resources, this.compileSchema()); + merge(resources, this.compileCustomDomain()); + merge(resources, this.compileCloudWatchLogGroup()); + merge(resources, this.compileLambdaAuthorizerPermission()); + merge(resources, this.compileWafRules()); + merge(resources, this.compileCachingResources()); //! requires naming + forEach(this.config.apiKeys, (key) => { + merge(resources, this.compileApiKey(key)); + }); } - merge(resources, this.compileEndpoint()); - merge(resources, this.compileSchema()); - merge(resources, this.compileCustomDomain()); - merge(resources, this.compileCloudWatchLogGroup()); - merge(resources, this.compileLambdaAuthorizerPermission()); - merge(resources, this.compileWafRules()); - merge(resources, this.compileCachingResources()); - forEach(this.config.apiKeys, (key) => { - merge(resources, this.compileApiKey(key)); - }); - forEach(this.config.dataSources, (ds) => { merge(resources, this.compileDataSource(ds)); }); @@ -86,9 +77,9 @@ export class Api { } compileEndpoint(): CfnResources { - if (this.isExistingApi()) { - return {}; - } + // as config is public, the type needs to be cheked every time + if (isSharedApiConfig(this.config)) return {}; + if (!this.naming) throw new Error('Unable to load naming'); const logicalId = this.naming.getApiLogicalId(); const endpointResource: CfnResource = { @@ -100,9 +91,7 @@ export class Api { EnvironmentVariables: this.config.environment, }, }; - // TODO : Handle the type properly - //! authentication is always required in this context - if (!this.config.authentication) return; + merge( endpointResource.Properties, this.compileAuthenticationProvider(this.config.authentication), @@ -162,12 +151,13 @@ export class Api { compileCloudWatchLogGroup(): CfnResources { if ( + isSharedApiConfig(this.config) || !this.config.logging || - this.config.logging.enabled === false || - this.isExistingApi() + this.config.logging.enabled === false ) { return {}; } + if (!this.naming) throw new Error('Unable to load naming'); const logGroupLogicalId = this.naming.getLogGroupLogicalId(); const roleLogicalId = this.naming.getLogGroupRoleLogicalId(); @@ -235,21 +225,22 @@ export class Api { } compileSchema() { - if (!this.config.schema || this.isExistingApi()) { - return {}; - } + if (isSharedApiConfig(this.config)) return {}; + if (!this.config.schema) return {}; // is this the expected behaviour ? + const schema = new Schema(this, this.config.schema); return schema.compile(); } compileCustomDomain(): CfnResources { + if (isSharedApiConfig(this.config)) return {}; + if (!this.naming) throw new Error('Unable to load naming'); const { domain } = this.config; if ( !domain || domain.enabled === false || - domain.useCloudFormation === false || - this.isExistingApi() + domain.useCloudFormation === false ) { return {}; } @@ -335,9 +326,10 @@ export class Api { } compileLambdaAuthorizerPermission(): CfnResources { - if (!this.config.authentication || this.isExistingApi()) { - return {}; - } + if (isSharedApiConfig(this.config)) return {}; + if (!this.naming) throw new Error('Unable to load naming'); + + if (!this.config.authentication) return {}; const lambdaAuth = [ ...this.config.additionalAuthentications, @@ -368,9 +360,9 @@ export class Api { } compileApiKey(config: ApiKeyConfig) { - if (this.isExistingApi()) { - return {}; - } + if (isSharedApiConfig(this.config)) return {}; + if (!this.naming) throw new Error('Unable to load naming'); + const { name, expiresAt, expiresAfter, description, apiKeyId } = config; const startOfHour = DateTime.now().setZone('UTC').startOf('hour'); @@ -419,11 +411,10 @@ export class Api { } compileCachingResources(): CfnResources { - if ( - !this.config.caching || - this.config.caching?.enabled === false || - this.isExistingApi() - ) { + if (isSharedApiConfig(this.config)) return {}; + if (!this.naming) throw new Error('Unable to load naming'); + + if (!this.config.caching || this.config.caching?.enabled === false) { return {}; } @@ -463,11 +454,9 @@ export class Api { } compileWafRules() { - if ( - !this.config.waf || - this.config.waf?.enabled === false || - this.isExistingApi() - ) { + if (isSharedApiConfig(this.config)) return {}; + + if (!this.config.waf || this.config.waf?.enabled === false) { return {}; } @@ -476,19 +465,16 @@ export class Api { } getApiId() { - if (this.config.apiId) { + if (isSharedApiConfig(this.config) && this.config.apiId) { return this.config.apiId; } + if (!this.naming) throw new Error('Unable to load naming'); const logicalIdGraphQLApi = this.naming.getApiLogicalId(); return { 'Fn::GetAtt': [logicalIdGraphQLApi, 'ApiId'], }; } - isExistingApi() { - return !!this.config?.apiId; - } - getUserPoolConfig(auth: CognitoAuth, isAdditionalAuth = false) { const userPoolConfig = { AwsRegion: auth.config.awsRegion || { 'Fn::Sub': '${AWS::Region}' }, @@ -522,6 +508,7 @@ export class Api { } getLambdaAuthorizerConfig(auth: LambdaAuth) { + if (!this.naming) throw new Error('Unable to load naming'); if (!auth.config) { return; } @@ -539,9 +526,8 @@ export class Api { } getTagsConfig() { - if (!this.config.tags || isEmpty(this.config.tags)) { - return undefined; - } + if (isSharedApiConfig(this.config)) return; + if (!this.config.tags || isEmpty(this.config.tags)) return; const tags = this.config.tags; return Object.keys(this.config.tags).map((key) => ({ diff --git a/src/resources/DataSource.ts b/src/resources/DataSource.ts index 5e3fd5cd..76e172e8 100644 --- a/src/resources/DataSource.ts +++ b/src/resources/DataSource.ts @@ -33,6 +33,7 @@ export class DataSource { resource.Properties.LambdaConfig = { LambdaFunctionArn: this.api.getLambdaArn( this.config.config, + // TODOD: Handle datasource from existing API this.api.naming.getDataSourceEmbeddedLambdaResolverName(this.config), ), }; diff --git a/src/resources/JsResolver.ts b/src/resources/JsResolver.ts index 0cc78480..ee10ef36 100644 --- a/src/resources/JsResolver.ts +++ b/src/resources/JsResolver.ts @@ -23,6 +23,7 @@ export class JsResolver { } getResolverContent(): string { + // Todod : handle js resolvers with config from the parent stack if (this.api.config.esbuild === false) { return fs.readFileSync(this.config.path, 'utf8'); } diff --git a/src/resources/PipelineFunction.ts b/src/resources/PipelineFunction.ts index 0b2ab2a1..a0579134 100644 --- a/src/resources/PipelineFunction.ts +++ b/src/resources/PipelineFunction.ts @@ -21,6 +21,7 @@ export class PipelineFunction { ); } + // Todo: HAndle Pipeline naming const logicalId = this.api.naming.getPipelineFunctionLogicalId( this.config.name, ); diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 35247b1d..8a81918f 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -58,6 +58,7 @@ export class Resolver { } } + // Todo: Handle cache config from parent resource if (this.config.caching) { if (this.config.caching === true) { // Use defaults @@ -85,6 +86,7 @@ export class Resolver { ); } + // TODO: handle resolver naming const logicalIdDataSource = this.api.naming.getDataSourceLogicalId(dataSource); Properties = { diff --git a/src/resources/Schema.ts b/src/resources/Schema.ts index 48452f11..0a7e0f2c 100644 --- a/src/resources/Schema.ts +++ b/src/resources/Schema.ts @@ -32,6 +32,7 @@ scalar AWSIPAddress export class Schema { constructor(private api: Api, private schemas: string[]) {} + // Todo : handle schema compile(): CfnResources { const logicalId = this.api.naming.getSchemaLogicalId(); diff --git a/src/resources/SyncConfig.ts b/src/resources/SyncConfig.ts index 893d2485..23c4c59f 100644 --- a/src/resources/SyncConfig.ts +++ b/src/resources/SyncConfig.ts @@ -7,6 +7,7 @@ export class SyncConfig { private config: ResolverConfig | PipelineFunctionConfig, ) {} + // Todo : handle sync naming compile() { if (!this.config.sync) { return undefined; diff --git a/src/resources/Waf.ts b/src/resources/Waf.ts index 2d2e0bc7..9d14a967 100644 --- a/src/resources/Waf.ts +++ b/src/resources/Waf.ts @@ -7,6 +7,7 @@ import { } from '../types/cloudFormation'; import { ApiKeyConfig, + isSharedApiConfig, WafConfig, WafRule, WafRuleAction, @@ -19,10 +20,15 @@ import { toCfnKeys } from '../utils'; export class Waf { constructor(private api: Api, private config: WafConfig) {} + // Todo: Handle Waf compile(): CfnResources { const wafConfig = this.config; - if (wafConfig.enabled === false) { - return {}; + if (wafConfig.enabled === false) return {}; + if (isSharedApiConfig(this.api.config)) { + throw Error('WAF cannot be specified on existing appsync apis'); + } + if (!this.api.naming) { + throw Error('Unable to load the naming module'); } const apiLogicalId = this.api.naming.getApiLogicalId(); const wafAssocLogicalId = this.api.naming.getWafAssociationLogicalId(); @@ -129,6 +135,9 @@ export class Waf { } buildApiKeysWafRules(): CfnWafRule[] { + if (isSharedApiConfig(this.api.config)) { + throw Error('WAF cannot be specified on existing appsync apis'); + } return ( reduce( this.api.config.apiKeys, @@ -139,11 +148,18 @@ export class Waf { } buildApiKeyRules(key: ApiKeyConfig) { - const rules = key.wafRules; + if (isSharedApiConfig(this.api.config)) { + throw Error('WAF cannot be specified on existing appsync apis'); + } + if (!this.api.naming) { + // I needed to change the loop to a forof loop to avoid making this check at every loop cycle + throw Error('Unable to load the naming module'); + } + const rules = key.wafRules ?? []; // Build the rule and add a matching rule for the X-Api-Key header // for the given api key const allRules: CfnWafRule[] = []; - rules?.forEach((keyRule) => { + for (const keyRule of rules) { const builtRule = this.buildWafRule(keyRule, key.name); const logicalIdApiKey = this.api.naming.getApiKeyLogicalId(key.name); const { Statement: baseStatement } = builtRule; @@ -198,7 +214,7 @@ export class Waf { ...builtRule, Statement: statement, }); - }); + } return allRules; } diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 05726e8e..d8c151a1 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -17,98 +17,48 @@ import type { Substitutions, EnvironmentVariables, } from './common'; -import type { IntrinsicFunction } from './cloudFormation'; +// import type { IntrinsicFunction } from './cloudFormation'; export * from './common'; -// TODO: Split into multiple configs -//? If most of the parameters are ignored when using apiId -//? you should define type like this to avoid misusage of the config, -//? it can be nice for those who use this type in their serverless.ts: -/* ts */ ` - export type BaseAppSyncConfig = { - dataSources: Record; - resolvers: Record; - pipelineFunctions: Record; - substitutions?: Substitutions; - caching?: CachingConfig; - }; - - export type NewAppSyncConfig = BaseAppSyncConfig & { - name: string; - schema: string[]; - authentication: Auth; - additionalAuthentications: Auth[]; - domain?: DomainConfig; - apiKeys?: Record; - xrayEnabled?: boolean; - logging?: LoggingConfig; - waf?: WafConfig; - tags?: Record; - }; - - export type ExistingAppSyncConfig = BaseAppSyncConfig & { - apiId: string | IntrinsicFunction; - }; - - export type AppSyncConfig = NewAppSyncConfig | ExistingAppSyncConfig; -`; - -//? I agree on this and it joins what I commented earlier. -//? The same should happen in the validation json schema. -//? I would use something like a union. -/* ts */ ` - export type BaseAppSyncConfig = { - dataSources: Record; - resolvers: Record; - pipelineFunctions: Record; - substitutions?: Substitutions; - caching?: CachingConfig; - }; - - export type FullAppSyncConfig = BaseAppSyncConfig & { - name: string; - schema: string[]; - authentication: Auth; - additionalAuthentications: Auth[]; - domain?: DomainConfig; - apiKeys?: Record; - xrayEnabled?: boolean; - logging?: LoggingConfig; - waf?: WafConfig; - tags?: Record; - }; - - export type SharedAppSyncConfig = BaseAppSyncConfig & { - apiId: string; - }; - - export type AppSyncConfig = FullAppSyncConfig | SharedAppSyncConfig -`; //! (not tested, might need adjustments) - -export type AppSyncConfig = { - name: string; - authentication?: Auth; - additionalAuthentications: Auth[]; - schema?: string[]; - domain?: DomainConfig; - apiKeys?: Record; +// TODO: The same should happen in the validation json schema. +export type BaseAppSyncConfig = { dataSources: Record; resolvers: Record; pipelineFunctions: Record; substitutions?: Substitutions; - environment?: EnvironmentVariables; +}; +export type FullAppSyncConfig = BaseAppSyncConfig & { + name: string; + schema?: string[]; + authentication: Auth; + additionalAuthentications: Auth[]; + domain?: DomainConfig; + apiKeys?: Record; xrayEnabled?: boolean; logging?: LoggingConfig; - caching?: CachingConfig; waf?: WafConfig; tags?: Record; - apiId?: string | IntrinsicFunction; + // TODO : Check that they can't be overriden in Shared AppSync + caching?: CachingConfig; + environment?: EnvironmentVariables; visibility?: 'GLOBAL' | 'PRIVATE'; esbuild?: BuildOptions | false; introspection?: boolean; queryDepthLimit?: number; resolverCountLimit?: number; }; +export type SharedAppSyncConfig = BaseAppSyncConfig & { + // TODO: Handle IntrinsicFunction + // apiId?: string | IntrinsicFunction; + apiId: string; +}; +export type AppSyncConfig = FullAppSyncConfig | SharedAppSyncConfig; + +export function isSharedApiConfig( + config: AppSyncConfig, +): config is SharedAppSyncConfig { + return 'apiId' in config; +} export type BaseResolverConfig = { field: string; From f385b737fef03e4b9749350ccb955e07522c051a Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 08:19:30 +0100 Subject: [PATCH 10/30] Added typegards in tests without changing their functionality --- src/__tests__/getAppSyncConfig.test.ts | 72 ++++++++++++++------------ 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/src/__tests__/getAppSyncConfig.test.ts b/src/__tests__/getAppSyncConfig.test.ts index db939541..53c81100 100644 --- a/src/__tests__/getAppSyncConfig.test.ts +++ b/src/__tests__/getAppSyncConfig.test.ts @@ -9,53 +9,59 @@ test('returns basic config', async () => { describe('Schema', () => { it('should return the default schema', () => { - expect( - getAppSyncConfig({ ...basicConfig, schema: undefined }).schema, - ).toMatchSnapshot(); + const config = getAppSyncConfig({ ...basicConfig, schema: undefined }); + const schema = 'schema' in config ? config.schema : undefined; + expect(schema).toMatchSnapshot(); }); it('should return a single schema as an array', () => { - expect( - getAppSyncConfig({ ...basicConfig, schema: 'mySchema.graphql' }).schema, - ).toMatchSnapshot(); + const config = getAppSyncConfig({ + ...basicConfig, + schema: 'mySchema.graphql', + }); + const schema = 'schema' in config ? config.schema : undefined; + expect(schema).toMatchSnapshot(); }); it('should return a schema array unchanged', () => { - expect( - getAppSyncConfig({ - ...basicConfig, - schema: ['users.graphql', 'posts.graphql'], - }).schema, - ).toMatchSnapshot(); + const config = getAppSyncConfig({ + ...basicConfig, + schema: ['users.graphql', 'posts.graphql'], + }); + const schema = 'schema' in config ? config.schema : undefined; + expect(schema).toMatchSnapshot(); }); }); describe('Api Keys', () => { it('should not generate a default Api Key when auth is not API_KEY', () => { - expect( - getAppSyncConfig({ ...basicConfig, authentication: { type: 'AWS_IAM' } }) - .apiKeys, - ).toBeUndefined(); + const config = getAppSyncConfig({ + ...basicConfig, + authentication: { type: 'AWS_IAM' }, + }); + const apiKeys = 'apiKeys' in config ? config.schema : undefined; + expect(apiKeys).toBeUndefined(); }); it('should generate api keys', () => { - expect( - getAppSyncConfig({ - ...basicConfig, - apiKeys: [ - { - name: 'John', - description: "John's key", - expiresAt: '2021-03-09T16:00:00+00:00', - }, - { - name: 'Jane', - expiresAfter: '1y', - }, - 'InlineKey', - ], - }).apiKeys, - ).toMatchInlineSnapshot(` + const config = getAppSyncConfig({ + ...basicConfig, + apiKeys: [ + { + name: 'John', + description: "John's key", + expiresAt: '2021-03-09T16:00:00+00:00', + }, + { + name: 'Jane', + expiresAfter: '1y', + }, + 'InlineKey', + ], + }); + const apiKeys = 'apiKeys' in config ? config.schema : undefined; + + expect(apiKeys).toMatchInlineSnapshot(` Object { "InlineKey": Object { "name": "InlineKey", From 38e039dfd78527edaf85d39acce1aa5144a844c2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 08:21:47 +0100 Subject: [PATCH 11/30] Handle SharedAppsyncConfig in getAppSyncConfig --- src/getAppSyncConfig.ts | 79 +++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/src/getAppSyncConfig.ts b/src/getAppSyncConfig.ts index c67e1882..dcb4c7a1 100644 --- a/src/getAppSyncConfig.ts +++ b/src/getAppSyncConfig.ts @@ -1,11 +1,11 @@ import { AppSyncConfig } from './types'; -import { +import type { ApiKeyConfig, AppSyncConfig as PluginAppSyncConfig, DataSourceConfig, PipelineFunctionConfig, ResolverConfig, - isSharedApiConfig, + BaseAppSyncConfig, SharedAppSyncConfig, FullAppSyncConfig, } from './types/plugin'; @@ -40,14 +40,57 @@ const toResourceName = (name: string) => { export const getAppSyncConfig = ( config: AppSyncConfig, ): PluginAppSyncConfig => { + const baseConfig = getBaseAppsyncConfig(config); + + // handle shared appsync config + if ('apiId' in config && config.apiId) { + // Todo : check after editing the validator + //? config: AppSyncConfig & Record<"apiId", unknown> + const apiId: string = config.apiId; + return { + ...baseConfig, + apiId, + } satisfies SharedAppSyncConfig; + } + + // Handle full appsync config const schema = Array.isArray(config.schema) ? config.schema : [config.schema || 'schema.graphql']; + const additionalAuthentications = config.additionalAuthentications || []; + + let apiKeys: Record | undefined; + if ( + config.authentication?.type === 'API_KEY' || + additionalAuthentications.some((auth) => auth.type === 'API_KEY') + ) { + const inputKeys = config.apiKeys || []; + + apiKeys = inputKeys.reduce((acc, key) => { + if (typeof key === 'string') { + acc[key] = { name: key }; + } else { + acc[key.name] = key; + } + + return acc; + }, {}); + } + + return { + ...config, + ...baseConfig, + additionalAuthentications, + apiKeys, + schema, + } satisfies FullAppSyncConfig; +}; + +function getBaseAppsyncConfig(config: AppSyncConfig): BaseAppSyncConfig { const dataSources: Record = {}; const resolvers: Record = {}; const pipelineFunctions: Record = {}; - const additionalAuthentications = config.additionalAuthentications || []; forEach(flattenMaps(config.dataSources), (ds, name) => { dataSources[name] = { @@ -130,35 +173,9 @@ export const getAppSyncConfig = ( }; }); - let apiKeys: Record | undefined; - if ( - config.authentication?.type === 'API_KEY' || - additionalAuthentications.some((auth) => auth.type === 'API_KEY') - ) { - const inputKeys = config.apiKeys || []; - - apiKeys = inputKeys.reduce((acc, key) => { - if (typeof key === 'string') { - acc[key] = { name: key }; - } else { - acc[key.name] = key; - } - - return acc; - }, {}); - } - - const appSyncConfig = { - ...config, - additionalAuthentications, - apiKeys, - schema, + return { dataSources, resolvers, pipelineFunctions, }; - - return isSharedApiConfig(appSyncConfig) - ? (appSyncConfig satisfies SharedAppSyncConfig) - : (appSyncConfig satisfies FullAppSyncConfig); -}; +} From 6cc691b5284d3ca4886f38cba7d0070abd3a01d2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 10:05:41 +0100 Subject: [PATCH 12/30] clean some todos --- src/index.ts | 9 ------- src/resources/Api.ts | 26 +++++++----------- src/resources/DataSource.ts | 5 +++- src/resources/JsResolver.ts | 8 ++++-- src/resources/PipelineFunction.ts | 5 +++- src/resources/Resolver.ts | 44 ++++++++++++++++++------------- src/resources/Schema.ts | 7 +++++ src/resources/SyncConfig.ts | 6 +++-- src/resources/Waf.ts | 1 - 9 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9bf5b498..69d7939f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -413,12 +413,6 @@ class ServerlessAppsyncPlugin { if (!apiId) { throw new this.serverless.classes.Error('Unable to get AppSync Api Id'); } - if (apiId !== 'string') { - // TODO : Handle IntrinsicFunction ? - throw new this.serverless.classes.Error( - 'AppSync apiId cannot be an IntrinsicFunction', - ); - } const { graphqlApi } = await this.provider.request< GetGraphqlApiRequest, @@ -455,9 +449,6 @@ class ServerlessAppsyncPlugin { // TODO : Check if the api key was provided from the config //! This function should not be run from a child service stacck - if (typeof apiId !== 'string') { - return; - } const { schema } = await this.provider.request< GetIntrospectionSchemaRequest, diff --git a/src/resources/Api.ts b/src/resources/Api.ts index f67b4d80..1d68b46c 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -32,16 +32,14 @@ import { log } from '@serverless/utils/log'; export class Api { public naming?: Naming; - public config: AppSyncConfig; public functions: Record> = {}; - constructor(config: AppSyncConfig, public plugin: ServerlessAppsyncPlugin) { - if ('apiId' in config) { - this.config = config satisfies SharedAppSyncConfig; - // Todo: check if naming is required here - } else { - this.config = config satisfies FullAppSyncConfig; - this.naming = new Naming(this.config.name); + constructor( + public config: AppSyncConfig, + public plugin: ServerlessAppsyncPlugin, + ) { + if ('name' in config) { + this.naming = new Naming(config.name); } } @@ -77,7 +75,7 @@ export class Api { } compileEndpoint(): CfnResources { - // as config is public, the type needs to be cheked every time + // in a class, the type needs to be cheked every time if (isSharedApiConfig(this.config)) return {}; if (!this.naming) throw new Error('Unable to load naming'); const logicalId = this.naming.getApiLogicalId(); @@ -588,16 +586,12 @@ export class Api { : lambdaArn; } - // TODO : Make those required (remove || {}) + // Todo: Same syntax for apiId ? hasDataSource(name: string) { - return name in (this.config.dataSources || {}); + return name in this.config.dataSources; } hasPipelineFunction(name: string) { - return name in (this.config.pipelineFunctions || {}); + return name in this.config.pipelineFunctions; } - //? I understand why you made those optional, but I'd rather keep them as required. - //? If you look here, those are actually already optional from a config point of view. - //? Then, getAppSyncConfig() makes sure to fill them with empty {} if needed for when it's injected in the compiler. - // https://github.com/sid88in/serverless-appsync-plugin/blob/05164d8847a554d56bb73590fdc35bf0bda5198e/src/getAppSyncConfig.ts#L36-L46 } diff --git a/src/resources/DataSource.ts b/src/resources/DataSource.ts index 76e172e8..f7e88c8b 100644 --- a/src/resources/DataSource.ts +++ b/src/resources/DataSource.ts @@ -33,7 +33,10 @@ export class DataSource { resource.Properties.LambdaConfig = { LambdaFunctionArn: this.api.getLambdaArn( this.config.config, - // TODOD: Handle datasource from existing API + // TODO: Handle datasource from existing API + //! Naming module should not be impacted here : + //! this is the datasource config, not the appsync config + //? Change why is this an object if we use it as static class ? this.api.naming.getDataSourceEmbeddedLambdaResolverName(this.config), ), }; diff --git a/src/resources/JsResolver.ts b/src/resources/JsResolver.ts index ee10ef36..f6a5ba88 100644 --- a/src/resources/JsResolver.ts +++ b/src/resources/JsResolver.ts @@ -1,6 +1,6 @@ import { IntrinsicFunction } from '../types/cloudFormation'; import fs from 'fs'; -import { Substitutions } from '../types/plugin'; +import { isSharedApiConfig, Substitutions } from '../types/plugin'; import { Api } from './Api'; import { buildSync } from 'esbuild'; @@ -23,7 +23,11 @@ export class JsResolver { } getResolverContent(): string { - // Todod : handle js resolvers with config from the parent stack + if (isSharedApiConfig(this.api.config)) { + // Todo : handle js resolvers with config from the parent stack + console.warn('esbuild config is ignored for shared appsync'); + return fs.readFileSync(this.config.path, 'utf8'); + } if (this.api.config.esbuild === false) { return fs.readFileSync(this.config.path, 'utf8'); } diff --git a/src/resources/PipelineFunction.ts b/src/resources/PipelineFunction.ts index a0579134..1b463563 100644 --- a/src/resources/PipelineFunction.ts +++ b/src/resources/PipelineFunction.ts @@ -21,7 +21,10 @@ export class PipelineFunction { ); } - // Todo: HAndle Pipeline naming + // Todo: HAndle Pipeline naming from existing API + //! Naming module should not be impacted here : + //! this is the datasource config, not the appsync config + //? Change why is this an object if we use it as static class ? const logicalId = this.api.naming.getPipelineFunctionLogicalId( this.config.name, ); diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 8a81918f..00423616 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -3,7 +3,7 @@ import { CfnResources, IntrinsicFunction, } from '../types/cloudFormation'; -import { ResolverConfig } from '../types/plugin'; +import { isSharedApiConfig, ResolverConfig } from '../types/plugin'; import { Api } from './Api'; import path from 'path'; import { MappingTemplate } from './MappingTemplate'; @@ -58,24 +58,29 @@ export class Resolver { } } - // Todo: Handle cache config from parent resource - if (this.config.caching) { - if (this.config.caching === true) { - // Use defaults - Properties.CachingConfig = { - Ttl: this.api.config.caching?.ttl || 3600, - }; - } else if (typeof this.config.caching === 'object') { - Properties.CachingConfig = { - CachingKeys: this.config.caching.keys, - Ttl: this.config.caching.ttl || this.api.config.caching?.ttl || 3600, - }; + if (isSharedApiConfig(this.api.config)) { + // Todo : handle resolvers caching & sync with config from the parent stack + console.warn('caching and sync config are ignored for shared appsync'); + } else { + if (this.config.caching) { + if (this.config.caching === true) { + // Use defaults + Properties.CachingConfig = { + Ttl: this.api.config.caching?.ttl || 3600, + }; + } else if (typeof this.config.caching === 'object') { + Properties.CachingConfig = { + CachingKeys: this.config.caching.keys, + Ttl: + this.config.caching.ttl || this.api.config.caching?.ttl || 3600, + }; + } } - } - if (this.config.sync) { - const asyncConfig = new SyncConfig(this.api, this.config); - Properties.SyncConfig = asyncConfig.compile(); + if (this.config.sync) { + const asyncConfig = new SyncConfig(this.api, this.config); + Properties.SyncConfig = asyncConfig.compile(); + } } if (this.config.kind === 'UNIT') { @@ -86,7 +91,10 @@ export class Resolver { ); } - // TODO: handle resolver naming + // TODO: handle resolver naming from existing API + //! Naming module should not be impacted here : + //! this is the datasource config, not the appsync config + //? Change why is this an object if we use it as static class ? const logicalIdDataSource = this.api.naming.getDataSourceLogicalId(dataSource); Properties = { diff --git a/src/resources/Schema.ts b/src/resources/Schema.ts index 0a7e0f2c..26f5e029 100644 --- a/src/resources/Schema.ts +++ b/src/resources/Schema.ts @@ -7,6 +7,7 @@ import { flatten } from 'lodash'; import { parse, print } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; import { mergeTypeDefs } from '@graphql-tools/merge'; +import { isSharedApiConfig } from '../types/plugin'; const AWS_TYPES = ` directive @aws_iam on FIELD_DEFINITION | OBJECT @@ -34,6 +35,12 @@ export class Schema { // Todo : handle schema compile(): CfnResources { + if (isSharedApiConfig(this.api.config)) { + throw Error('Unable to override shared api schemas'); + } + if (!this.api.naming) { + throw Error('Unable to load the naming module'); + } const logicalId = this.api.naming.getSchemaLogicalId(); return { diff --git a/src/resources/SyncConfig.ts b/src/resources/SyncConfig.ts index 23c4c59f..7492eb43 100644 --- a/src/resources/SyncConfig.ts +++ b/src/resources/SyncConfig.ts @@ -1,4 +1,4 @@ -import { PipelineFunctionConfig, ResolverConfig } from '../types/plugin'; +import { isSharedApiConfig, PipelineFunctionConfig, ResolverConfig } from '../types/plugin'; import { Api } from './Api'; export class SyncConfig { @@ -7,8 +7,10 @@ export class SyncConfig { private config: ResolverConfig | PipelineFunctionConfig, ) {} - // Todo : handle sync naming compile() { + if (isSharedApiConfig(this.api.config)) { + throw Error('Unable to set the sync config for a Shared AppsyncApi'); + } if (!this.config.sync) { return undefined; } diff --git a/src/resources/Waf.ts b/src/resources/Waf.ts index 9d14a967..85a71276 100644 --- a/src/resources/Waf.ts +++ b/src/resources/Waf.ts @@ -20,7 +20,6 @@ import { toCfnKeys } from '../utils'; export class Waf { constructor(private api: Api, private config: WafConfig) {} - // Todo: Handle Waf compile(): CfnResources { const wafConfig = this.config; if (wafConfig.enabled === false) return {}; From fe2061ddf6d4795ac5d60c93009b2ce94d727287 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 17:34:02 +0100 Subject: [PATCH 13/30] Updated types and validation --- src/getAppSyncConfig.ts | 6 +- src/index.ts | 2 +- src/resources/DataSource.ts | 17 +- src/resources/Naming.ts | 54 +- src/resources/PipelineFunction.ts | 11 +- src/resources/Resolver.ts | 33 +- src/resources/Schema.ts | 1 - src/resources/SyncConfig.ts | 9 +- src/types/index.ts | 39 +- src/types/plugin.ts | 29 +- src/validation.ts | 951 +++--------------------------- src/validation/definitions.ts | 655 ++++++++++++++++++++ src/validation/properties.ts | 243 ++++++++ 13 files changed, 1093 insertions(+), 957 deletions(-) create mode 100644 src/validation/definitions.ts create mode 100644 src/validation/properties.ts diff --git a/src/getAppSyncConfig.ts b/src/getAppSyncConfig.ts index dcb4c7a1..f343a3d3 100644 --- a/src/getAppSyncConfig.ts +++ b/src/getAppSyncConfig.ts @@ -1,4 +1,4 @@ -import { AppSyncConfig } from './types'; +import { AppSyncConfig, isSharedApiConfig } from './types'; import type { ApiKeyConfig, AppSyncConfig as PluginAppSyncConfig, @@ -43,9 +43,7 @@ export const getAppSyncConfig = ( const baseConfig = getBaseAppsyncConfig(config); // handle shared appsync config - if ('apiId' in config && config.apiId) { - // Todo : check after editing the validator - //? config: AppSyncConfig & Record<"apiId", unknown> + if (isSharedApiConfig(config)) { const apiId: string = config.apiId; return { ...baseConfig, diff --git a/src/index.ts b/src/index.ts index 69d7939f..5fa12757 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1118,4 +1118,4 @@ class ServerlessAppsyncPlugin { } } -export = ServerlessAppsyncPlugin; +export default ServerlessAppsyncPlugin; diff --git a/src/resources/DataSource.ts b/src/resources/DataSource.ts index f7e88c8b..70d4e177 100644 --- a/src/resources/DataSource.ts +++ b/src/resources/DataSource.ts @@ -14,6 +14,7 @@ import { DsEventBridgeConfig, } from '../types/plugin'; import { Api } from './Api'; +import { Naming } from './Naming'; export class DataSource { constructor(private api: Api, private config: DataSourceConfig) {} @@ -33,11 +34,7 @@ export class DataSource { resource.Properties.LambdaConfig = { LambdaFunctionArn: this.api.getLambdaArn( this.config.config, - // TODO: Handle datasource from existing API - //! Naming module should not be impacted here : - //! this is the datasource config, not the appsync config - //? Change why is this an object if we use it as static class ? - this.api.naming.getDataSourceEmbeddedLambdaResolverName(this.config), + Naming.getDataSourceEmbeddedLambdaResolverName(this.config), ), }; } else if (this.config.type === 'AMAZON_DYNAMODB') { @@ -58,7 +55,7 @@ export class DataSource { ); } - const logicalId = this.api.naming.getDataSourceLogicalId(this.config.name); + const logicalId = Naming.getDataSourceLogicalId(this.config.name); const resources = { [logicalId]: resource, @@ -69,7 +66,7 @@ export class DataSource { } else { const role = this.compileDataSourceIamRole(); if (role) { - const roleLogicalId = this.api.naming.getDataSourceRoleLogicalId( + const roleLogicalId = Naming.getDataSourceRoleLogicalId( this.config.name, ); resource.Properties.ServiceRoleArn = { @@ -224,9 +221,7 @@ export class DataSource { return; } - const logicalId = this.api.naming.getDataSourceRoleLogicalId( - this.config.name, - ); + const logicalId = Naming.getDataSourceRoleLogicalId(this.config.name); return { [logicalId]: { @@ -263,7 +258,7 @@ export class DataSource { case 'AWS_LAMBDA': { const lambdaArn = this.api.getLambdaArn( this.config.config, - this.api.naming.getDataSourceEmbeddedLambdaResolverName(this.config), + Naming.getDataSourceEmbeddedLambdaResolverName(this.config), ); // Allow "invoke" for the Datasource's function and its aliases/versions diff --git a/src/resources/Naming.ts b/src/resources/Naming.ts index d3b3cacb..0b8c5aa0 100644 --- a/src/resources/Naming.ts +++ b/src/resources/Naming.ts @@ -7,89 +7,87 @@ import { export class Naming { constructor(private apiName: string) {} - getCfnName(name: string) { + static getCfnName(name: string) { return name.replace(/[^a-zA-Z0-9]/g, ''); } - getLogicalId(name: string): string { - return this.getCfnName(name); + static getLogicalId(name: string): string { + return Naming.getCfnName(name); } getApiLogicalId() { - return this.getLogicalId(`GraphQlApi`); + return Naming.getLogicalId(`GraphQlApi`); } getSchemaLogicalId() { - return this.getLogicalId(`GraphQlSchema`); + return Naming.getLogicalId(`GraphQlSchema`); } getDomainNameLogicalId() { - return this.getLogicalId(`GraphQlDomainName`); + return Naming.getLogicalId(`GraphQlDomainName`); } getDomainCertificateLogicalId() { - return this.getLogicalId(`GraphQlDomainCertificate`); + return Naming.getLogicalId(`GraphQlDomainCertificate`); } getDomainAssociationLogicalId() { - return this.getLogicalId(`GraphQlDomainAssociation`); + return Naming.getLogicalId(`GraphQlDomainAssociation`); } getDomainReoute53RecordLogicalId() { - return this.getLogicalId(`GraphQlDomainRoute53Record`); + return Naming.getLogicalId(`GraphQlDomainRoute53Record`); } getLogGroupLogicalId() { - return this.getLogicalId(`GraphQlApiLogGroup`); + return Naming.getLogicalId(`GraphQlApiLogGroup`); } getLogGroupRoleLogicalId() { - return this.getLogicalId(`GraphQlApiLogGroupRole`); + return Naming.getLogicalId(`GraphQlApiLogGroupRole`); } getLogGroupPolicyLogicalId() { - return this.getLogicalId(`GraphQlApiLogGroupPolicy`); + return Naming.getLogicalId(`GraphQlApiLogGroupPolicy`); } getCachingLogicalId() { - return this.getLogicalId(`GraphQlCaching`); + return Naming.getLogicalId(`GraphQlCaching`); } getLambdaAuthLogicalId() { - return this.getLogicalId(`LambdaAuthorizerPermission`); + return Naming.getLogicalId(`LambdaAuthorizerPermission`); } getApiKeyLogicalId(name: string) { - return this.getLogicalId(`GraphQlApi${name}`); + return Naming.getLogicalId(`GraphQlApi${name}`); } - // Warning: breaking change. - // api name added - getDataSourceLogicalId(name: string) { - return `GraphQlDs${this.getLogicalId(name)}`; + static getDataSourceLogicalId(name: string) { + return `GraphQlDs${Naming.getLogicalId(name)}`; } - getDataSourceRoleLogicalId(name: string) { - return this.getDataSourceLogicalId(`${name}Role`); + static getDataSourceRoleLogicalId(name: string) { + return Naming.getDataSourceLogicalId(`${name}Role`); } - getResolverLogicalId(type: string, field: string) { - return this.getLogicalId(`GraphQlResolver${type}${field}`); + static getResolverLogicalId(type: string, field: string) { + return Naming.getLogicalId(`GraphQlResolver${type}${field}`); } - getPipelineFunctionLogicalId(name: string) { - return this.getLogicalId(`GraphQlFunctionConfiguration${name}`); + static getPipelineFunctionLogicalId(name: string) { + return Naming.getLogicalId(`GraphQlFunctionConfiguration${name}`); } getWafLogicalId() { - return this.getLogicalId('GraphQlWaf'); + return Naming.getLogicalId('GraphQlWaf'); } getWafAssociationLogicalId() { - return this.getLogicalId('GraphQlWafAssoc'); + return Naming.getLogicalId('GraphQlWafAssoc'); } - getDataSourceEmbeddedLambdaResolverName(config: DataSourceConfig) { + static getDataSourceEmbeddedLambdaResolverName(config: DataSourceConfig) { return config.name; } diff --git a/src/resources/PipelineFunction.ts b/src/resources/PipelineFunction.ts index 1b463563..d56d123f 100644 --- a/src/resources/PipelineFunction.ts +++ b/src/resources/PipelineFunction.ts @@ -9,6 +9,7 @@ import path from 'path'; import { MappingTemplate } from './MappingTemplate'; import { SyncConfig } from './SyncConfig'; import { JsResolver } from './JsResolver'; +import { Naming } from './Naming'; export class PipelineFunction { constructor(private api: Api, private config: PipelineFunctionConfig) {} @@ -21,14 +22,8 @@ export class PipelineFunction { ); } - // Todo: HAndle Pipeline naming from existing API - //! Naming module should not be impacted here : - //! this is the datasource config, not the appsync config - //? Change why is this an object if we use it as static class ? - const logicalId = this.api.naming.getPipelineFunctionLogicalId( - this.config.name, - ); - const logicalIdDataSource = this.api.naming.getDataSourceLogicalId( + const logicalId = Naming.getPipelineFunctionLogicalId(this.config.name); + const logicalIdDataSource = Naming.getDataSourceLogicalId( this.config.dataSource, ); diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 00423616..0d78a901 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -1,5 +1,6 @@ import { CfnResolver, + CfnResource, CfnResources, IntrinsicFunction, } from '../types/cloudFormation'; @@ -9,6 +10,7 @@ import path from 'path'; import { MappingTemplate } from './MappingTemplate'; import { SyncConfig } from './SyncConfig'; import { JsResolver } from './JsResolver'; +import { Naming } from './Naming'; // A decent default for pipeline JS resolvers const DEFAULT_JS_RESOLVERS = ` @@ -91,12 +93,7 @@ export class Resolver { ); } - // TODO: handle resolver naming from existing API - //! Naming module should not be impacted here : - //! this is the datasource config, not the appsync config - //? Change why is this an object if we use it as static class ? - const logicalIdDataSource = - this.api.naming.getDataSourceLogicalId(dataSource); + const logicalIdDataSource = Naming.getDataSourceLogicalId(dataSource); Properties = { ...Properties, Kind: 'UNIT', @@ -116,27 +113,31 @@ export class Resolver { } const logicalIdDataSource = - this.api.naming.getPipelineFunctionLogicalId(name); + Naming.getPipelineFunctionLogicalId(name); return { 'Fn::GetAtt': [logicalIdDataSource, 'FunctionId'] }; }), }, }; } - const logicalIdResolver = this.api.naming.getResolverLogicalId( + const logicalResolver: CfnResource = { + Type: 'AWS::AppSync::Resolver', + Properties, + }; + + // Add dependacy to the schema for the full appsync configs + if (!isSharedApiConfig(this.api.config)) { + if (!this.api.naming) throw Error('Unable to load the naming module'); + logicalResolver.DependsOn = [this.api.naming.getSchemaLogicalId()]; + } + + const logicalIdResolver = Naming.getResolverLogicalId( this.config.type, this.config.field, ); - const logicalIdGraphQLSchema = this.api.naming.getSchemaLogicalId(); return { - [logicalIdResolver]: { - Type: 'AWS::AppSync::Resolver', - ...(!this.api.isExistingApi() && { - DependsOn: [logicalIdGraphQLSchema], - }), - Properties, - }, + [logicalIdResolver]: logicalResolver, }; } diff --git a/src/resources/Schema.ts b/src/resources/Schema.ts index 26f5e029..2013394c 100644 --- a/src/resources/Schema.ts +++ b/src/resources/Schema.ts @@ -33,7 +33,6 @@ scalar AWSIPAddress export class Schema { constructor(private api: Api, private schemas: string[]) {} - // Todo : handle schema compile(): CfnResources { if (isSharedApiConfig(this.api.config)) { throw Error('Unable to override shared api schemas'); diff --git a/src/resources/SyncConfig.ts b/src/resources/SyncConfig.ts index 7492eb43..65d4bcab 100644 --- a/src/resources/SyncConfig.ts +++ b/src/resources/SyncConfig.ts @@ -1,4 +1,8 @@ -import { isSharedApiConfig, PipelineFunctionConfig, ResolverConfig } from '../types/plugin'; +import { + isSharedApiConfig, + PipelineFunctionConfig, + ResolverConfig, +} from '../types/plugin'; import { Api } from './Api'; export class SyncConfig { @@ -11,6 +15,9 @@ export class SyncConfig { if (isSharedApiConfig(this.api.config)) { throw Error('Unable to set the sync config for a Shared AppsyncApi'); } + if (!this.api.naming) { + throw Error('Unable to Load the naming module'); + } if (!this.config.sync) { return undefined; } diff --git a/src/types/index.ts b/src/types/index.ts index e52f8388..e5f5b3b2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ -import { BuildOptions } from 'esbuild'; -import { +//* External typing : Used in serverless.ts +import type { BuildOptions } from 'esbuild'; +import type { ApiKeyConfig, Auth, Substitutions, @@ -11,29 +12,31 @@ import { DsEventBridgeConfig, DsHttpConfig, DsLambdaConfig, - DsNone, DsOpenSearchConfig, + DsNone, DsRelationalDbConfig, SyncConfig, EnvironmentVariables, } from './common'; export * from './common'; -export type AppSyncConfig = { +type BaseAppSyncConfig = { + dataSources: + | Record[] + | Record; + resolvers?: Record[] | Record; + pipelineFunctions?: + | Record[] + | Record; + substitutions?: Substitutions; +}; +export type FullAppSyncConfig = BaseAppSyncConfig & { name: string; schema?: string | string[]; authentication: Auth; additionalAuthentications?: Auth[]; domain?: DomainConfig; apiKeys?: (ApiKeyConfig | string)[]; - resolvers?: Record[] | Record; - pipelineFunctions?: - | Record[] - | Record; - dataSources: - | Record[] - | Record; - substitutions?: Substitutions; environment?: EnvironmentVariables; xrayEnabled?: boolean; logging?: LoggingConfig; @@ -47,6 +50,18 @@ export type AppSyncConfig = { resolverCountLimit?: number; }; +export type SharedAppSyncConfig = BaseAppSyncConfig & { + apiId: string; +}; + +export function isSharedApiConfig( + config: AppSyncConfig, +): config is SharedAppSyncConfig { + return 'apiId' in config; +} + +export type AppSyncConfig = FullAppSyncConfig | SharedAppSyncConfig; + export type BaseResolverConfig = { field?: string; type?: string; diff --git a/src/types/plugin.ts b/src/types/plugin.ts index d8c151a1..ecbc1b6e 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -1,23 +1,23 @@ +//* Internal typing : Used in the plugin exclusively import type { BuildOptions } from 'esbuild'; import type { + ApiKeyConfig, Auth, + Substitutions, + CachingConfig, DomainConfig, - ApiKeyConfig, LoggingConfig, - CachingConfig, WafConfig, - SyncConfig, - DsHttpConfig, DsDynamoDBConfig, - DsRelationalDbConfig, - DsOpenSearchConfig, - DsLambdaConfig, DsEventBridgeConfig, + DsHttpConfig, + DsLambdaConfig, + DsOpenSearchConfig, DsNone, - Substitutions, + DsRelationalDbConfig, + SyncConfig, EnvironmentVariables, } from './common'; -// import type { IntrinsicFunction } from './cloudFormation'; export * from './common'; // TODO: The same should happen in the validation json schema. @@ -34,13 +34,12 @@ export type FullAppSyncConfig = BaseAppSyncConfig & { additionalAuthentications: Auth[]; domain?: DomainConfig; apiKeys?: Record; + environment?: EnvironmentVariables; xrayEnabled?: boolean; logging?: LoggingConfig; + caching?: CachingConfig; waf?: WafConfig; tags?: Record; - // TODO : Check that they can't be overriden in Shared AppSync - caching?: CachingConfig; - environment?: EnvironmentVariables; visibility?: 'GLOBAL' | 'PRIVATE'; esbuild?: BuildOptions | false; introspection?: boolean; @@ -48,8 +47,6 @@ export type FullAppSyncConfig = BaseAppSyncConfig & { resolverCountLimit?: number; }; export type SharedAppSyncConfig = BaseAppSyncConfig & { - // TODO: Handle IntrinsicFunction - // apiId?: string | IntrinsicFunction; apiId: string; }; export type AppSyncConfig = FullAppSyncConfig | SharedAppSyncConfig; @@ -90,7 +87,7 @@ export type PipelineResolverConfig = BaseResolverConfig & { }; export type DataSourceConfig = { - name: string; + name: string; // Not avalible in external types (index.ts) description?: string; } & ( | DsHttpConfig @@ -103,7 +100,7 @@ export type DataSourceConfig = { ); export type PipelineFunctionConfig = { - name: string; + name: string; // Not avalible in external types (index.ts) dataSource: string; description?: string; code?: string; diff --git a/src/validation.ts b/src/validation.ts index 05dbae98..6dd0dcba 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -2,870 +2,64 @@ import Ajv from 'ajv'; import ajvErrors from 'ajv-errors'; import ajvMergePatch from 'ajv-merge-patch'; import addFormats from 'ajv-formats'; -import { timeUnits } from './utils'; +import * as definitions from './validation/definitions'; +import * as properties from './validation/properties'; -const AUTH_TYPES = [ - 'AMAZON_COGNITO_USER_POOLS', - 'AWS_LAMBDA', - 'OPENID_CONNECT', - 'AWS_IAM', - 'API_KEY', -] as const; - -const DATASOURCE_TYPES = [ - 'AMAZON_DYNAMODB', - 'AMAZON_OPENSEARCH_SERVICE', - 'AWS_LAMBDA', - 'HTTP', - 'NONE', - 'RELATIONAL_DATABASE', - 'AMAZON_EVENTBRIDGE', -] as const; - -// TODO: Split in 2 schemas -//? as mentioned above, we should have 2 schemas: one for a usual appsync config (main api or single stack) -//? and one for "external API" which only accepts supporters fields. -//? This way, authentication can remain required in single stack scenarios -//? or for main stack and be "forbidden" for sub stacks -export const appSyncSchema = { +export const fullAppSyncSchema = { type: 'object', definitions: { - stringOrIntrinsicFunction: { - oneOf: [ - { type: 'string' }, - { - type: 'object', - required: [], - additionalProperties: true, - }, - ], - errorMessage: 'must be a string or a CloudFormation intrinsic function', - }, - lambdaFunctionConfig: { - oneOf: [ - { - type: 'object', - properties: { - functionName: { type: 'string' }, - functionAlias: { type: 'string' }, - }, - required: ['functionName'], - }, - { - type: 'object', - properties: { - functionArn: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - }, - required: ['functionArn'], - }, - { - type: 'object', - properties: { - function: { type: 'object' }, - }, - required: ['function'], - }, - ], - errorMessage: - 'must specify functionName, functionArn or function (all exclusives)', - }, - auth: { - type: 'object', - title: 'Authentication', - description: 'Authentication type and definition', - properties: { - type: { - type: 'string', - enum: AUTH_TYPES, - errorMessage: `must be one of ${AUTH_TYPES.join(', ')}`, - }, - }, - if: { properties: { type: { const: 'AMAZON_COGNITO_USER_POOLS' } } }, - then: { - properties: { config: { $ref: '#/definitions/cognitoAuth' } }, - required: ['config'], - }, - else: { - if: { properties: { type: { const: 'AWS_LAMBDA' } } }, - then: { - properties: { config: { $ref: '#/definitions/lambdaAuth' } }, - required: ['config'], - }, - else: { - if: { properties: { type: { const: 'OPENID_CONNECT' } } }, - then: { - properties: { config: { $ref: '#/definitions/oidcAuth' } }, - required: ['config'], - }, - }, - }, - required: ['type'], - }, - cognitoAuth: { - type: 'object', - properties: { - userPoolId: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - awsRegion: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - defaultAction: { - type: 'string', - enum: ['ALLOW', 'DENY'], - errorMessage: 'must be "ALLOW" or "DENY"', - }, - appIdClientRegex: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - }, - required: ['userPoolId'], - }, - lambdaAuth: { - type: 'object', - oneOf: [{ $ref: '#/definitions/lambdaFunctionConfig' }], - properties: { - // Note: functionName and functionArn are already defined in #/definitions/lambdaFunctionConfig - // But if not also defined here, TypeScript shows an error. - functionName: { type: 'string' }, - functionArn: { type: 'string' }, - identityValidationExpression: { type: 'string' }, - authorizerResultTtlInSeconds: { type: 'number' }, - }, - required: [], - }, - oidcAuth: { - type: 'object', - properties: { - issuer: { type: 'string' }, - clientId: { type: 'string' }, - iatTTL: { type: 'number' }, - authTTL: { type: 'number' }, - }, - required: ['issuer'], - }, - iamAuth: { - type: 'object', - properties: { - type: { - type: 'string', - const: 'AWS_IAM', - }, - }, - required: ['type'], - errorMessage: 'must be a valid IAM config', - }, - apiKeyAuth: { - type: 'object', - properties: { - type: { - type: 'string', - const: 'API_KEY', - }, - }, - required: ['type'], - errorMessage: 'must be a valid API_KEY config', - }, - visibilityConfig: { - type: 'object', - properties: { - cloudWatchMetricsEnabled: { type: 'boolean' }, - name: { type: 'string' }, - sampledRequestsEnabled: { type: 'boolean' }, - }, - required: [], - }, - wafRule: { - anyOf: [ - { type: 'string', enum: ['throttle', 'disableIntrospection'] }, - { - type: 'object', - properties: { - disableIntrospection: { - type: 'object', - properties: { - name: { type: 'string' }, - priority: { type: 'integer' }, - visibilityConfig: { $ref: '#/definitions/visibilityConfig' }, - }, - }, - }, - required: ['disableIntrospection'], - }, - { - type: 'object', - properties: { - throttle: { - oneOf: [ - { type: 'integer', minimum: 100 }, - { - type: 'object', - properties: { - name: { type: 'string' }, - action: { - type: 'string', - enum: ['Allow', 'Block'], - }, - aggregateKeyType: { - type: 'string', - enum: ['IP', 'FORWARDED_IP'], - }, - limit: { type: 'integer', minimum: 100 }, - priority: { type: 'integer' }, - scopeDownStatement: { type: 'object' }, - forwardedIPConfig: { - type: 'object', - properties: { - headerName: { - type: 'string', - pattern: '^[a-zA-Z0-9-]+$', - }, - fallbackBehavior: { - type: 'string', - enum: ['MATCH', 'NO_MATCH'], - }, - }, - required: ['headerName', 'fallbackBehavior'], - }, - visibilityConfig: { - $ref: '#/definitions/visibilityConfig', - }, - }, - required: [], - }, - ], - }, - }, - required: ['throttle'], - }, - { $ref: '#/definitions/customWafRule' }, - ], - errorMessage: 'must be a valid WAF rule', - }, - customWafRule: { - type: 'object', - properties: { - name: { type: 'string' }, - priority: { type: 'number' }, - action: { - type: 'string', - enum: ['Allow', 'Block', 'Count', 'Captcha'], - }, - statement: { type: 'object', required: [] }, - visibilityConfig: { $ref: '#/definitions/visibilityConfig' }, - }, - required: ['name', 'statement'], - }, - substitutions: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - required: [], - errorMessage: 'must be a valid substitutions definition', - }, - environment: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - required: [], - errorMessage: 'must be a valid environment definition', - }, - dataSource: { - if: { type: 'object' }, - then: { $ref: '#/definitions/dataSourceConfig' }, - else: { - type: 'string', - errorMessage: 'must be a string or data source definition', - }, - }, - resolverConfig: { - type: 'object', - properties: { - kind: { - type: 'string', - enum: ['UNIT', 'PIPELINE'], - errorMessage: 'must be "UNIT" or "PIPELINE"', - }, - type: { type: 'string' }, - field: { type: 'string' }, - maxBatchSize: { type: 'number', minimum: 1, maximum: 2000 }, - code: { type: 'string' }, - request: { type: 'string' }, - response: { type: 'string' }, - sync: { $ref: '#/definitions/syncConfig' }, - substitutions: { $ref: '#/definitions/substitutions' }, - caching: { $ref: '#/definitions/resolverCachingConfig' }, - }, - if: { properties: { kind: { const: 'UNIT' } }, required: ['kind'] }, - then: { - properties: { - dataSource: { $ref: '#/definitions/dataSource' }, - }, - required: ['dataSource'], - }, - else: { - properties: { - functions: { - type: 'array', - items: { $ref: '#/definitions/pipelineFunction' }, - }, - }, - required: ['functions'], - }, - required: [], - }, - resolverConfigMap: { - type: 'object', - patternProperties: { - // Type.field keys, type and field are not required - '^[_A-Za-z][_0-9A-Za-z]*\\.[_A-Za-z][_0-9A-Za-z]*$': { - $ref: '#/definitions/resolverConfig', - }, - }, - additionalProperties: { - // Other keys, type and field are required - $merge: { - source: { $ref: '#/definitions/resolverConfig' }, - with: { required: ['type', 'field'] }, - }, - errorMessage: { - required: { - type: 'resolver definitions that do not specify Type.field in the key must specify the type and field as properties', - field: - 'resolver definitions that do not specify Type.field in the key must specify the type and field as properties', - }, - }, - }, - required: [], - }, - pipelineFunctionConfig: { - type: 'object', - properties: { - dataSource: { $ref: '#/definitions/dataSource' }, - description: { type: 'string' }, - request: { type: 'string' }, - response: { type: 'string' }, - sync: { $ref: '#/definitions/syncConfig' }, - maxBatchSize: { type: 'number', minimum: 1, maximum: 2000 }, - substitutions: { $ref: '#/definitions/substitutions' }, - }, - required: ['dataSource'], - }, - pipelineFunction: { - if: { type: 'object' }, - then: { $ref: '#/definitions/pipelineFunctionConfig' }, - else: { - type: 'string', - errorMessage: 'must be a string or function definition', - }, - }, - pipelineFunctionConfigMap: { - type: 'object', - additionalProperties: { - if: { type: 'object' }, - then: { $ref: '#/definitions/pipelineFunctionConfig' }, - else: { - type: 'string', - errorMessage: 'must be a string or an object', - }, - }, - required: [], - }, - resolverCachingConfig: { - oneOf: [ - { type: 'boolean' }, - { - type: 'object', - properties: { - ttl: { type: 'integer', minimum: 1, maximum: 3600 }, - keys: { - type: 'array', - items: { type: 'string' }, - }, - }, - required: [], - }, - ], - errorMessage: 'must be a valid resolver caching config', - }, - syncConfig: { - type: 'object', - if: { properties: { conflictHandler: { const: ['LAMBDA'] } } }, - then: { $ref: '#/definitions/lambdaFunctionConfig' }, - properties: { - functionArn: { type: 'string' }, - functionName: { type: 'string' }, - conflictDetection: { type: 'string', enum: ['VERSION', 'NONE'] }, - conflictHandler: { - type: 'string', - enum: ['LAMBDA', 'OPTIMISTIC_CONCURRENCY', 'AUTOMERGE'], - }, - }, - required: [], - }, - iamRoleStatements: { - type: 'array', - items: { - type: 'object', - properties: { - Effect: { type: 'string', enum: ['Allow', 'Deny'] }, - Action: { type: 'array', items: { type: 'string' } }, - Resource: { - oneOf: [ - { $ref: '#/definitions/stringOrIntrinsicFunction' }, - { - type: 'array', - items: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - }, - ], - errorMessage: 'contains invalid resolver definitions', - }, - }, - required: ['Effect', 'Action', 'Resource'], - errorMessage: 'must be a valid IAM role statement', - }, - }, - dataSourceConfig: { - type: 'object', - properties: { - type: { - type: 'string', - enum: DATASOURCE_TYPES, - errorMessage: `must be one of ${DATASOURCE_TYPES.join(', ')}`, - }, - description: { type: 'string' }, - }, - if: { properties: { type: { const: 'AMAZON_DYNAMODB' } } }, - then: { - properties: { config: { $ref: '#/definitions/dataSourceDynamoDb' } }, - required: ['config'], - }, - else: { - if: { properties: { type: { const: 'AWS_LAMBDA' } } }, - then: { - properties: { - config: { $ref: '#/definitions/datasourceLambdaConfig' }, - }, - required: ['config'], - }, - else: { - if: { properties: { type: { const: 'HTTP' } } }, - then: { - properties: { - config: { $ref: '#/definitions/dataSourceHttpConfig' }, - }, - required: ['config'], - }, - else: { - if: { - properties: { - type: { const: 'AMAZON_OPENSEARCH_SERVICE' }, - }, - }, - then: { - properties: { - config: { $ref: '#/definitions/datasourceEsConfig' }, - }, - required: ['config'], - }, - else: { - if: { properties: { type: { const: 'RELATIONAL_DATABASE' } } }, - then: { - properties: { - config: { - $ref: '#/definitions/datasourceRelationalDbConfig', - }, - }, - required: ['config'], - }, - else: { - if: { properties: { type: { const: 'AMAZON_EVENTBRIDGE' } } }, - then: { - properties: { - config: { - $ref: '#/definitions/datasourceEventBridgeConfig', - }, - }, - required: ['config'], - }, - }, - }, - }, - }, - }, - required: ['type'], - }, - dataSourceHttpConfig: { - type: 'object', - properties: { - endpoint: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - serviceRoleArn: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - iamRoleStatements: { - $ref: '#/definitions/iamRoleStatements', - }, - authorizationConfig: { - type: 'object', - properties: { - authorizationType: { - type: 'string', - enum: ['AWS_IAM'], - errorMessage: 'must be AWS_IAM', - }, - awsIamConfig: { - type: 'object', - properties: { - signingRegion: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - signingServiceName: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - }, - required: ['signingRegion'], - }, - }, - required: ['authorizationType', 'awsIamConfig'], - }, - }, - required: ['endpoint'], - }, - dataSourceDynamoDb: { - type: 'object', - properties: { - tableName: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - useCallerCredentials: { type: 'boolean' }, - serviceRoleArn: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - region: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - iamRoleStatements: { - $ref: '#/definitions/iamRoleStatements', - }, - versioned: { type: 'boolean' }, - deltaSyncConfig: { - type: 'object', - properties: { - deltaSyncTableName: { type: 'string' }, - baseTableTTL: { type: 'integer' }, - deltaSyncTableTTL: { type: 'integer' }, - }, - required: ['deltaSyncTableName'], - }, - }, - required: ['tableName'], - }, - datasourceRelationalDbConfig: { - type: 'object', - properties: { - region: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - relationalDatabaseSourceType: { - type: 'string', - enum: ['RDS_HTTP_ENDPOINT'], - }, - serviceRoleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - dbClusterIdentifier: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - databaseName: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - schema: { type: 'string' }, - awsSecretStoreArn: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - iamRoleStatements: { - $ref: '#/definitions/iamRoleStatements', - }, - }, - required: ['awsSecretStoreArn', 'dbClusterIdentifier'], - }, - datasourceLambdaConfig: { - type: 'object', - oneOf: [ - { - $ref: '#/definitions/lambdaFunctionConfig', - }, - ], - properties: { - functionName: { type: 'string' }, - functionArn: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - serviceRoleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - iamRoleStatements: { $ref: '#/definitions/iamRoleStatements' }, - }, - required: [], - }, - datasourceEsConfig: { - type: 'object', - oneOf: [ - { - oneOf: [ - { - type: 'object', - properties: { - endpoint: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - }, - required: ['endpoint'], - }, - { - type: 'object', - properties: { - domain: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - }, - required: ['domain'], - }, - ], - errorMessage: 'must have a endpoint or domain (but not both)', - }, - ], - properties: { - endpoint: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - domain: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - region: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - serviceRoleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - iamRoleStatements: { $ref: '#/definitions/iamRoleStatements' }, - }, - required: [], - }, - datasourceEventBridgeConfig: { - type: 'object', - properties: { - eventBusArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - }, - required: ['eventBusArn'], - }, + stringOrIntrinsicFunction: definitions.stringOrIntrinsicFunction, + lambdaFunctionConfig: definitions.lambdaFunctionConfig, + auth: definitions.auth, + cognitoAuth: definitions.cognitoAuth, + lambdaAuth: definitions.lambdaAuth, + oidcAuth: definitions.oidcAuth, + iamAuth: definitions.iamAuth, + apiKeyAuth: definitions.apiKeyAuth, + visibilityConfig: definitions.visibilityConfig, + wafRule: definitions.wafRule, + customWafRule: definitions.customWafRule, + substitutions: definitions.substitutions, + environment: definitions.environment, + dataSource: definitions.dataSource, + resolverConfig: definitions.resolverConfig, + resolverConfigMap: definitions.resolverConfigMap, + pipelineFunctionConfig: definitions.pipelineFunctionConfig, + pipelineFunction: definitions.pipelineFunction, + pipelineFunctionConfigMap: definitions.pipelineFunctionConfigMap, + resolverCachingConfig: definitions.resolverCachingConfig, + syncConfig: definitions.syncConfig, + iamRoleStatements: definitions.iamRoleStatements, + dataSourceConfig: definitions.dataSourceConfig, + dataSourceHttpConfig: definitions.dataSourceHttpConfig, + dataSourceDynamoDb: definitions.dataSourceDynamoDb, + datasourceRelationalDbConfig: definitions.datasourceRelationalDbConfig, + datasourceLambdaConfig: definitions.datasourceLambdaConfig, + datasourceEsConfig: definitions.datasourceEsConfig, + datasourceEventBridgeConfig: definitions.datasourceEventBridgeConfig, }, properties: { - name: { type: 'string' }, - authentication: { $ref: '#/definitions/auth' }, - schema: { - anyOf: [ - { - type: 'string', - }, - { - type: 'array', - items: { type: 'string' }, - }, - ], - errorMessage: 'must be a valid schema config', - }, - domain: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - useCloudFormation: { type: 'boolean' }, - retain: { type: 'boolean' }, - name: { - type: 'string', - pattern: '^([a-z][a-z0-9+-]*\\.)+[a-z][a-z0-9]*$', - errorMessage: 'must be a valid domain name', - }, - certificateArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - hostedZoneId: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - hostedZoneName: { - type: 'string', - pattern: '^([a-z][a-z0-9+-]*\\.)+[a-z][a-z0-9]*\\.$', - errorMessage: - 'must be a valid zone name. Note: you must include a trailing dot (eg: `example.com.`)', - }, - route53: { type: 'boolean' }, - }, - required: ['name'], - if: { - anyOf: [ - { - not: { properties: { useCloudFormation: { const: false } } }, - }, - { not: { required: ['useCloudFormation'] } }, - ], - }, - then: { - anyOf: [ - { required: ['certificateArn'] }, - { required: ['hostedZoneId'] }, - ], - errorMessage: - 'when using CloudFormation, you must provide either certificateArn or hostedZoneId.', - }, - }, - xrayEnabled: { type: 'boolean' }, - visibility: { - type: 'string', - enum: ['GLOBAL', 'PRIVATE'], - errorMessage: 'must be "GLOBAL" or "PRIVATE"', - }, - introspection: { type: 'boolean' }, - queryDepthLimit: { type: 'integer', minimum: 1, maximum: 75 }, - resolverCountLimit: { type: 'integer', minimum: 1, maximum: 1000 }, - substitutions: { $ref: '#/definitions/substitutions' }, - environment: { $ref: '#/definitions/environment' }, - waf: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - }, - if: { - required: ['arn'], - }, - then: { - properties: { - arn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - }, - }, - else: { - properties: { - name: { type: 'string' }, - defaultAction: { - type: 'string', - enum: ['Allow', 'Block'], - errorMessage: "must be 'Allow' or 'Block'", - }, - description: { type: 'string' }, - rules: { - type: 'array', - items: { $ref: '#/definitions/wafRule' }, - }, - }, - required: ['rules'], - }, - }, - tags: { - type: 'object', - additionalProperties: { type: 'string' }, - }, - caching: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - behavior: { - type: 'string', - enum: ['FULL_REQUEST_CACHING', 'PER_RESOLVER_CACHING'], - errorMessage: - "must be one of 'FULL_REQUEST_CACHING', 'PER_RESOLVER_CACHING'", - }, - type: { - enum: [ - 'SMALL', - 'MEDIUM', - 'LARGE', - 'XLARGE', - 'LARGE_2X', - 'LARGE_4X', - 'LARGE_8X', - 'LARGE_12X', - ], - errorMessage: - "must be one of 'SMALL', 'MEDIUM', 'LARGE', 'XLARGE', 'LARGE_2X', 'LARGE_4X', 'LARGE_8X', 'LARGE_12X'", - }, - ttl: { type: 'integer', minimum: 1, maximum: 3600 }, - atRestEncryption: { type: 'boolean' }, - transitEncryption: { type: 'boolean' }, - }, - required: ['behavior'], - }, - additionalAuthentications: { - type: 'array', - items: { $ref: '#/definitions/auth' }, - }, - apiKeys: { - type: 'array', - items: { - if: { type: 'object' }, - then: { - type: 'object', - properties: { - name: { type: 'string' }, - description: { type: 'string' }, - expiresAfter: { - type: ['string', 'number'], - pattern: `^(\\d+)(${Object.keys(timeUnits).join('|')})?$`, - errorMessage: 'must be a valid duration.', - }, - expiresAt: { - type: 'string', - format: 'date-time', - errorMessage: 'must be a valid date-time', - }, - wafRules: { - type: 'array', - items: { $ref: '#/definitions/wafRule' }, - }, - }, - required: ['name'], - }, - else: { - type: 'string', - }, - }, - }, - logging: { - type: 'object', - properties: { - roleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, - level: { - type: 'string', - enum: ['ALL', 'INFO', 'DEBUG', 'ERROR', 'NONE'], - errorMessage: - "must be one of 'ALL', 'INFO', 'DEBUG', 'ERROR' or 'NONE'", - }, - retentionInDays: { type: 'integer' }, - excludeVerboseContent: { type: 'boolean' }, - enabled: { type: 'boolean' }, - }, - required: ['level'], - }, - dataSources: { - oneOf: [ - { - type: 'object', - additionalProperties: { $ref: '#/definitions/dataSourceConfig' }, - }, - { - type: 'array', - items: { - type: 'object', - additionalProperties: { $ref: '#/definitions/dataSourceConfig' }, - }, - }, - ], - errorMessage: 'contains invalid data source definitions', - }, - resolvers: { - oneOf: [ - { $ref: '#/definitions/resolverConfigMap' }, - { - type: 'array', - items: { $ref: '#/definitions/resolverConfigMap' }, - }, - ], - errorMessage: 'contains invalid resolver definitions', - }, - pipelineFunctions: { - oneOf: [ - { - $ref: '#/definitions/pipelineFunctionConfigMap', - }, - { - type: 'array', - items: { - $ref: '#/definitions/pipelineFunctionConfigMap', - }, - }, - ], - errorMessage: 'contains invalid pipeline function definitions', - }, - esbuild: { - oneOf: [ - { - type: 'object', - }, - { const: false }, - ], - errorMessage: 'must be an esbuild config object or false', - }, - apiId: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + name: properties.name, + authentication: properties.authentication, + schema: properties.schema, + domain: properties.domain, + xrayEnabled: properties.xrayEnabled, + visibility: properties.visibility, + introspection: properties.introspection, + queryDepthLimit: properties.queryDepthLimit, + resolverCountLimit: properties.resolverCountLimit, + substitutions: properties.substitutions, + environment: properties.environment, + waf: properties.waf, + tags: properties.tags, + caching: properties.caching, + additionalAuthentications: properties.additionalAuthentications, + apiKeys: properties.apiKeys, + logging: properties.logging, + dataSources: properties.dataSources, + resolvers: properties.resolvers, + pipelineFunctions: properties.pipelineFunctions, + esbuild: properties.esbuild, }, required: ['name'], additionalProperties: { @@ -873,6 +67,45 @@ export const appSyncSchema = { errorMessage: 'invalid (unknown) property', }, }; +export const sharedAppSyncSchema = { + type: 'object', + definitions: { + stringOrIntrinsicFunction: definitions.stringOrIntrinsicFunction, + lambdaFunctionConfig: definitions.lambdaFunctionConfig, + substitutions: definitions.substitutions, + dataSource: definitions.dataSource, + resolverConfig: definitions.resolverConfig, + resolverConfigMap: definitions.resolverConfigMap, + pipelineFunctionConfig: definitions.pipelineFunctionConfig, + pipelineFunction: definitions.pipelineFunction, + pipelineFunctionConfigMap: definitions.pipelineFunctionConfigMap, + resolverCachingConfig: definitions.resolverCachingConfig, + iamRoleStatements: definitions.iamRoleStatements, + dataSourceConfig: definitions.dataSourceConfig, + dataSourceHttpConfig: definitions.dataSourceHttpConfig, + dataSourceDynamoDb: definitions.dataSourceDynamoDb, + datasourceRelationalDbConfig: definitions.datasourceRelationalDbConfig, + datasourceLambdaConfig: definitions.datasourceLambdaConfig, + datasourceEsConfig: definitions.datasourceEsConfig, + datasourceEventBridgeConfig: definitions.datasourceEventBridgeConfig, + }, + properties: { + substitutions: properties.substitutions, + dataSources: properties.dataSources, + resolvers: properties.resolvers, + pipelineFunctions: properties.pipelineFunctions, + apiId: { type: 'string' }, // properties.apiId, // TODO: Handle intrinsic function + }, + required: ['apiId'], + additionalProperties: { + not: true, + errorMessage: 'invalid (unknown) property', + }, +}; + +const appSyncSchema = { + oneOf: [sharedAppSyncSchema, fullAppSyncSchema], +}; const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); ajvMergePatch(ajv); diff --git a/src/validation/definitions.ts b/src/validation/definitions.ts new file mode 100644 index 00000000..09bb375b --- /dev/null +++ b/src/validation/definitions.ts @@ -0,0 +1,655 @@ +export const AUTH_TYPES = [ + 'AMAZON_COGNITO_USER_POOLS', + 'AWS_LAMBDA', + 'OPENID_CONNECT', + 'AWS_IAM', + 'API_KEY', +] as const; + +export const DATASOURCE_TYPES = [ + 'AMAZON_DYNAMODB', + 'AMAZON_OPENSEARCH_SERVICE', + 'AWS_LAMBDA', + 'HTTP', + 'NONE', + 'RELATIONAL_DATABASE', + 'AMAZON_EVENTBRIDGE', +] as const; + +export const stringOrIntrinsicFunction = { + oneOf: [ + { type: 'string' }, + { + type: 'object', + required: [], + additionalProperties: true, + }, + ], + errorMessage: 'must be a string or a CloudFormation intrinsic function', +}; +// Depends on stringOrIntrinsicFunction +export const lambdaFunctionConfig = { + oneOf: [ + { + type: 'object', + properties: { + functionName: { type: 'string' }, + functionAlias: { type: 'string' }, + }, + required: ['functionName'], + }, + { + type: 'object', + properties: { + functionArn: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + }, + required: ['functionArn'], + }, + { + type: 'object', + properties: { + function: { type: 'object' }, + }, + required: ['function'], + }, + ], + errorMessage: + 'must specify functionName, functionArn or function (all exclusives)', +}; + +//depends on cognitoAuth, lambdaAuth and oidcAuth +export const auth = { + type: 'object', + title: 'Authentication', + description: 'Authentication type and definition', + properties: { + type: { + type: 'string', + enum: AUTH_TYPES, + errorMessage: `must be one of ${AUTH_TYPES.join(', ')}`, + }, + }, + if: { properties: { type: { const: 'AMAZON_COGNITO_USER_POOLS' } } }, + then: { + properties: { config: { $ref: '#/definitions/cognitoAuth' } }, + required: ['config'], + }, + else: { + if: { properties: { type: { const: 'AWS_LAMBDA' } } }, + then: { + properties: { config: { $ref: '#/definitions/lambdaAuth' } }, + required: ['config'], + }, + else: { + if: { properties: { type: { const: 'OPENID_CONNECT' } } }, + then: { + properties: { config: { $ref: '#/definitions/oidcAuth' } }, + required: ['config'], + }, + }, + }, + required: ['type'], +}; + +// Depends on stringOrIntrinsicFunction +export const cognitoAuth = { + type: 'object', + properties: { + userPoolId: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + awsRegion: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + defaultAction: { + type: 'string', + enum: ['ALLOW', 'DENY'], + errorMessage: 'must be "ALLOW" or "DENY"', + }, + appIdClientRegex: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + }, + required: ['userPoolId'], +}; + +// Depends on lambdaFunctionConfig +export const lambdaAuth = { + type: 'object', + oneOf: [{ $ref: '#/definitions/lambdaFunctionConfig' }], + properties: { + // Note: functionName and functionArn are already defined in #/definitions/lambdaFunctionConfig + // But if not also defined here, TypeScript shows an error. + functionName: { type: 'string' }, + functionArn: { type: 'string' }, + identityValidationExpression: { type: 'string' }, + authorizerResultTtlInSeconds: { type: 'number' }, + }, + required: [], +}; + +export const oidcAuth = { + type: 'object', + properties: { + issuer: { type: 'string' }, + clientId: { type: 'string' }, + iatTTL: { type: 'number' }, + authTTL: { type: 'number' }, + }, + required: ['issuer'], +}; + +export const iamAuth = { + type: 'object', + properties: { + type: { + type: 'string', + const: 'AWS_IAM', + }, + }, + required: ['type'], + errorMessage: 'must be a valid IAM config', +}; + +export const apiKeyAuth = { + type: 'object', + properties: { + type: { + type: 'string', + const: 'API_KEY', + }, + }, + required: ['type'], + errorMessage: 'must be a valid API_KEY config', +}; + +export const visibilityConfig = { + type: 'object', + properties: { + cloudWatchMetricsEnabled: { type: 'boolean' }, + name: { type: 'string' }, + sampledRequestsEnabled: { type: 'boolean' }, + }, + required: [], +}; + +// Depends on visibilityConfig and customWafRule +export const wafRule = { + anyOf: [ + { type: 'string', enum: ['throttle', 'disableIntrospection'] }, + { + type: 'object', + properties: { + disableIntrospection: { + type: 'object', + properties: { + name: { type: 'string' }, + priority: { type: 'integer' }, + visibilityConfig: { $ref: '#/definitions/visibilityConfig' }, + }, + }, + }, + required: ['disableIntrospection'], + }, + { + type: 'object', + properties: { + throttle: { + oneOf: [ + { type: 'integer', minimum: 100 }, + { + type: 'object', + properties: { + name: { type: 'string' }, + action: { + type: 'string', + enum: ['Allow', 'Block'], + }, + aggregateKeyType: { + type: 'string', + enum: ['IP', 'FORWARDED_IP'], + }, + limit: { type: 'integer', minimum: 100 }, + priority: { type: 'integer' }, + scopeDownStatement: { type: 'object' }, + forwardedIPConfig: { + type: 'object', + properties: { + headerName: { + type: 'string', + pattern: '^[a-zA-Z0-9-]+$', + }, + fallbackBehavior: { + type: 'string', + enum: ['MATCH', 'NO_MATCH'], + }, + }, + required: ['headerName', 'fallbackBehavior'], + }, + visibilityConfig: { + $ref: '#/definitions/visibilityConfig', + }, + }, + required: [], + }, + ], + }, + }, + required: ['throttle'], + }, + { $ref: '#/definitions/customWafRule' }, + ], + errorMessage: 'must be a valid WAF rule', +}; +// Depends on visibilityConfig +export const customWafRule = { + type: 'object', + properties: { + name: { type: 'string' }, + priority: { type: 'number' }, + action: { + type: 'string', + enum: ['Allow', 'Block', 'Count', 'Captcha'], + }, + statement: { type: 'object', required: [] }, + visibilityConfig: { $ref: '#/definitions/visibilityConfig' }, + }, + required: ['name', 'statement'], +}; + +// Depends on stringOrIntrinsicFunction +export const substitutions = { + type: 'object', + additionalProperties: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + required: [], + errorMessage: 'must be a valid substitutions definition', +}; +// Depends on stringOrIntrinsicFunction +export const environment = { + type: 'object', + additionalProperties: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + required: [], + errorMessage: 'must be a valid environment definition', +}; +// Depends on dataSourceConfig +export const dataSource = { + if: { type: 'object' }, + then: { $ref: '#/definitions/dataSourceConfig' }, + else: { + type: 'string', + errorMessage: 'must be a string or data source definition', + }, +}; +// Depends on substitutions, resolverCachingConfig, dataSource, pipelineFunction +export const resolverConfig = { + type: 'object', + properties: { + kind: { + type: 'string', + enum: ['UNIT', 'PIPELINE'], + errorMessage: 'must be "UNIT" or "PIPELINE"', + }, + type: { type: 'string' }, + field: { type: 'string' }, + maxBatchSize: { type: 'number', minimum: 1, maximum: 2000 }, + code: { type: 'string' }, + request: { type: 'string' }, + response: { type: 'string' }, + sync: { $ref: '#/definitions/syncConfig' }, + substitutions: { $ref: '#/definitions/substitutions' }, + caching: { $ref: '#/definitions/resolverCachingConfig' }, + }, + if: { properties: { kind: { const: 'UNIT' } }, required: ['kind'] }, + then: { + properties: { + dataSource: { $ref: '#/definitions/dataSource' }, + }, + required: ['dataSource'], + }, + else: { + properties: { + functions: { + type: 'array', + items: { $ref: '#/definitions/pipelineFunction' }, + }, + }, + required: ['functions'], + }, + required: [], +}; +// Depends on resolverConfig +export const resolverConfigMap = { + type: 'object', + patternProperties: { + // Type.field keys, type and field are not required + '^[_A-Za-z][_0-9A-Za-z]*\\.[_A-Za-z][_0-9A-Za-z]*$': { + $ref: '#/definitions/resolverConfig', + }, + }, + additionalProperties: { + // Other keys, type and field are required + $merge: { + source: { $ref: '#/definitions/resolverConfig' }, + with: { required: ['type', 'field'] }, + }, + errorMessage: { + required: { + type: 'resolver definitions that do not specify Type.field in the key must specify the type and field as properties', + field: + 'resolver definitions that do not specify Type.field in the key must specify the type and field as properties', + }, + }, + }, + required: [], +}; +// Depends on dataSource +export const pipelineFunctionConfig = { + type: 'object', + properties: { + dataSource: { $ref: '#/definitions/dataSource' }, + description: { type: 'string' }, + request: { type: 'string' }, + response: { type: 'string' }, + sync: { $ref: '#/definitions/syncConfig' }, + maxBatchSize: { type: 'number', minimum: 1, maximum: 2000 }, + substitutions: { $ref: '#/definitions/substitutions' }, + }, + required: ['dataSource'], +}; +// Depends on pipelineFunctionConfig +export const pipelineFunction = { + if: { type: 'object' }, + then: { $ref: '#/definitions/pipelineFunctionConfig' }, + else: { + type: 'string', + errorMessage: 'must be a string or function definition', + }, +}; +// Depends on pipelineFunctionConfig +export const pipelineFunctionConfigMap = { + type: 'object', + additionalProperties: { + if: { type: 'object' }, + then: { $ref: '#/definitions/pipelineFunctionConfig' }, + else: { + type: 'string', + errorMessage: 'must be a string or an object', + }, + }, + required: [], +}; +export const resolverCachingConfig = { + oneOf: [ + { type: 'boolean' }, + { + type: 'object', + properties: { + ttl: { type: 'integer', minimum: 1, maximum: 3600 }, + keys: { + type: 'array', + items: { type: 'string' }, + }, + }, + required: [], + }, + ], + errorMessage: 'must be a valid resolver caching config', +}; +// Depends on lambdaFunctionConfig +export const syncConfig = { + type: 'object', + if: { properties: { conflictHandler: { const: ['LAMBDA'] } } }, + then: { $ref: '#/definitions/lambdaFunctionConfig' }, + properties: { + functionArn: { type: 'string' }, + functionName: { type: 'string' }, + conflictDetection: { type: 'string', enum: ['VERSION', 'NONE'] }, + conflictHandler: { + type: 'string', + enum: ['LAMBDA', 'OPTIMISTIC_CONCURRENCY', 'AUTOMERGE'], + }, + }, + required: [], +}; +// Depends on stringOrIntrinsicFunction +export const iamRoleStatements = { + type: 'array', + items: { + type: 'object', + properties: { + Effect: { type: 'string', enum: ['Allow', 'Deny'] }, + Action: { type: 'array', items: { type: 'string' } }, + Resource: { + oneOf: [ + { $ref: '#/definitions/stringOrIntrinsicFunction' }, + { + type: 'array', + items: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + }, + ], + errorMessage: 'contains invalid resolver definitions', + }, + }, + required: ['Effect', 'Action', 'Resource'], + errorMessage: 'must be a valid IAM role statement', + }, +}; +// Depends on dataSourceDynamoDb, datasourceLambdaConfig, dataSourceHttpConfig, datasourceEsConfig, datasourceRelationalDbConfig and datasourceEventBridgeConfig +export const dataSourceConfig = { + type: 'object', + properties: { + type: { + type: 'string', + enum: DATASOURCE_TYPES, + errorMessage: `must be one of ${DATASOURCE_TYPES.join(', ')}`, + }, + description: { type: 'string' }, + }, + if: { properties: { type: { const: 'AMAZON_DYNAMODB' } } }, + then: { + properties: { config: { $ref: '#/definitions/dataSourceDynamoDb' } }, + required: ['config'], + }, + else: { + if: { properties: { type: { const: 'AWS_LAMBDA' } } }, + then: { + properties: { + config: { $ref: '#/definitions/datasourceLambdaConfig' }, + }, + required: ['config'], + }, + else: { + if: { properties: { type: { const: 'HTTP' } } }, + then: { + properties: { + config: { $ref: '#/definitions/dataSourceHttpConfig' }, + }, + required: ['config'], + }, + else: { + if: { + properties: { + type: { const: 'AMAZON_OPENSEARCH_SERVICE' }, + }, + }, + then: { + properties: { + config: { $ref: '#/definitions/datasourceEsConfig' }, + }, + required: ['config'], + }, + else: { + if: { properties: { type: { const: 'RELATIONAL_DATABASE' } } }, + then: { + properties: { + config: { + $ref: '#/definitions/datasourceRelationalDbConfig', + }, + }, + required: ['config'], + }, + else: { + if: { properties: { type: { const: 'AMAZON_EVENTBRIDGE' } } }, + then: { + properties: { + config: { + $ref: '#/definitions/datasourceEventBridgeConfig', + }, + }, + required: ['config'], + }, + }, + }, + }, + }, + }, + required: ['type'], +}; +// Depends on stringOrIntrinsicFunction and iamRoleStatements +export const dataSourceHttpConfig = { + type: 'object', + properties: { + endpoint: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + serviceRoleArn: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + iamRoleStatements: { + $ref: '#/definitions/iamRoleStatements', + }, + authorizationConfig: { + type: 'object', + properties: { + authorizationType: { + type: 'string', + enum: ['AWS_IAM'], + errorMessage: 'must be AWS_IAM', + }, + awsIamConfig: { + type: 'object', + properties: { + signingRegion: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + signingServiceName: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + }, + required: ['signingRegion'], + }, + }, + required: ['authorizationType', 'awsIamConfig'], + }, + }, + required: ['endpoint'], +}; +// Depends on stringOrIntrinsicFunction and iamRoleStatements +export const dataSourceDynamoDb = { + type: 'object', + properties: { + tableName: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + useCallerCredentials: { type: 'boolean' }, + serviceRoleArn: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + region: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + iamRoleStatements: { + $ref: '#/definitions/iamRoleStatements', + }, + versioned: { type: 'boolean' }, + deltaSyncConfig: { + type: 'object', + properties: { + deltaSyncTableName: { type: 'string' }, + baseTableTTL: { type: 'integer' }, + deltaSyncTableTTL: { type: 'integer' }, + }, + required: ['deltaSyncTableName'], + }, + }, + required: ['tableName'], +}; +// Depends on stringOrIntrinsicFunction and iamRoleStatements +export const datasourceRelationalDbConfig = { + type: 'object', + properties: { + region: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + relationalDatabaseSourceType: { + type: 'string', + enum: ['RDS_HTTP_ENDPOINT'], + }, + serviceRoleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + dbClusterIdentifier: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + databaseName: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + schema: { type: 'string' }, + awsSecretStoreArn: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + iamRoleStatements: { + $ref: '#/definitions/iamRoleStatements', + }, + }, + required: ['awsSecretStoreArn', 'dbClusterIdentifier'], +}; +// Depends on lambdaFunctionConfig, stringOrIntrinsicFunction and iamRoleStatements +export const datasourceLambdaConfig = { + type: 'object', + oneOf: [ + { + $ref: '#/definitions/lambdaFunctionConfig', + }, + ], + properties: { + functionName: { type: 'string' }, + functionArn: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + serviceRoleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + iamRoleStatements: { $ref: '#/definitions/iamRoleStatements' }, + }, + required: [], +}; +// Depends on stringOrIntrinsicFunction and iamRoleStatements +export const datasourceEsConfig = { + type: 'object', + oneOf: [ + { + oneOf: [ + { + type: 'object', + properties: { + endpoint: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + }, + required: ['endpoint'], + }, + { + type: 'object', + properties: { + domain: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + }, + required: ['domain'], + }, + ], + errorMessage: 'must have a endpoint or domain (but not both)', + }, + ], + properties: { + endpoint: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + domain: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + region: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + serviceRoleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + iamRoleStatements: { $ref: '#/definitions/iamRoleStatements' }, + }, + required: [], +}; +// Depends on stringOrIntrinsicFunction +export const datasourceEventBridgeConfig = { + type: 'object', + properties: { + eventBusArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + }, + required: ['eventBusArn'], +}; diff --git a/src/validation/properties.ts b/src/validation/properties.ts new file mode 100644 index 00000000..273c2176 --- /dev/null +++ b/src/validation/properties.ts @@ -0,0 +1,243 @@ +import { timeUnits } from '../utils'; + +export const name = { type: 'string' }; +// Depends on auth +export const authentication = { $ref: '#/definitions/auth' }; +export const schema = { + anyOf: [ + { + type: 'string', + }, + { + type: 'array', + items: { type: 'string' }, + }, + ], + errorMessage: 'must be a valid schema config', +}; +//Depends on stringOrIntrinsicFunction +export const domain = { + type: 'object', + properties: { + enabled: { type: 'boolean' }, + useCloudFormation: { type: 'boolean' }, + retain: { type: 'boolean' }, + name: { + type: 'string', + pattern: '^([a-z][a-z0-9+-]*\\.)+[a-z][a-z0-9]*$', + errorMessage: 'must be a valid domain name', + }, + certificateArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + hostedZoneId: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + hostedZoneName: { + type: 'string', + pattern: '^([a-z][a-z0-9+-]*\\.)+[a-z][a-z0-9]*\\.$', + errorMessage: + 'must be a valid zone name. Note: you must include a trailing dot (eg: `example.com.`)', + }, + route53: { type: 'boolean' }, + }, + required: ['name'], + if: { + anyOf: [ + { + not: { properties: { useCloudFormation: { const: false } } }, + }, + { not: { required: ['useCloudFormation'] } }, + ], + }, + then: { + anyOf: [{ required: ['certificateArn'] }, { required: ['hostedZoneId'] }], + errorMessage: + 'when using CloudFormation, you must provide either certificateArn or hostedZoneId.', + }, +}; +export const xrayEnabled = { type: 'boolean' }; +export const visibility = { + type: 'string', + enum: ['GLOBAL', 'PRIVATE'], + errorMessage: 'must be "GLOBAL" or "PRIVATE"', +}; +export const introspection = { type: 'boolean' }; +export const queryDepthLimit = { type: 'integer', minimum: 1, maximum: 75 }; +export const resolverCountLimit = { + type: 'integer', + minimum: 1, + maximum: 1000, +}; +// Depends on substitutions +export const substitutions = { $ref: '#/definitions/substitutions' }; +// Depends on environment +export const environment = { $ref: '#/definitions/environment' }; +// Depends on stringOrIntrinsicFunction and wafRule +export const waf = { + type: 'object', + properties: { + enabled: { type: 'boolean' }, + }, + if: { + required: ['arn'], + }, + then: { + properties: { + arn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + }, + }, + else: { + properties: { + name: { type: 'string' }, + defaultAction: { + type: 'string', + enum: ['Allow', 'Block'], + errorMessage: "must be 'Allow' or 'Block'", + }, + description: { type: 'string' }, + rules: { + type: 'array', + items: { $ref: '#/definitions/wafRule' }, + }, + }, + required: ['rules'], + }, +}; +export const tags = { + type: 'object', + additionalProperties: { type: 'string' }, +}; +export const caching = { + type: 'object', + properties: { + enabled: { type: 'boolean' }, + behavior: { + type: 'string', + enum: ['FULL_REQUEST_CACHING', 'PER_RESOLVER_CACHING'], + errorMessage: + "must be one of 'FULL_REQUEST_CACHING', 'PER_RESOLVER_CACHING'", + }, + type: { + enum: [ + 'SMALL', + 'MEDIUM', + 'LARGE', + 'XLARGE', + 'LARGE_2X', + 'LARGE_4X', + 'LARGE_8X', + 'LARGE_12X', + ], + errorMessage: + "must be one of 'SMALL', 'MEDIUM', 'LARGE', 'XLARGE', 'LARGE_2X', 'LARGE_4X', 'LARGE_8X', 'LARGE_12X'", + }, + ttl: { type: 'integer', minimum: 1, maximum: 3600 }, + atRestEncryption: { type: 'boolean' }, + transitEncryption: { type: 'boolean' }, + }, + required: ['behavior'], +}; +// Depends on auth +export const additionalAuthentications = { + type: 'array', + items: { $ref: '#/definitions/auth' }, +}; +// Depends on wafRule +export const apiKeys = { + type: 'array', + items: { + if: { type: 'object' }, + then: { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + expiresAfter: { + type: ['string', 'number'], + pattern: `^(\\d+)(${Object.keys(timeUnits).join('|')})?$`, + errorMessage: 'must be a valid duration.', + }, + expiresAt: { + type: 'string', + format: 'date-time', + errorMessage: 'must be a valid date-time', + }, + wafRules: { + type: 'array', + items: { $ref: '#/definitions/wafRule' }, + }, + }, + required: ['name'], + }, + else: { + type: 'string', + }, + }, +}; +// Depends on stringOrIntrinsicFunction +export const logging = { + type: 'object', + properties: { + roleArn: { $ref: '#/definitions/stringOrIntrinsicFunction' }, + level: { + type: 'string', + enum: ['ALL', 'INFO', 'DEBUG', 'ERROR', 'NONE'], + errorMessage: "must be one of 'ALL', 'INFO', 'DEBUG', 'ERROR' or 'NONE'", + }, + retentionInDays: { type: 'integer' }, + excludeVerboseContent: { type: 'boolean' }, + enabled: { type: 'boolean' }, + }, + required: ['level'], +}; +// Depends on dataSourceConfig +export const dataSources = { + oneOf: [ + { + type: 'object', + additionalProperties: { $ref: '#/definitions/dataSourceConfig' }, + }, + { + type: 'array', + items: { + type: 'object', + additionalProperties: { $ref: '#/definitions/dataSourceConfig' }, + }, + }, + ], + errorMessage: 'contains invalid data source definitions', +}; +// Depends on resolverConfigMap +export const resolvers = { + oneOf: [ + { $ref: '#/definitions/resolverConfigMap' }, + { + type: 'array', + items: { $ref: '#/definitions/resolverConfigMap' }, + }, + ], + errorMessage: 'contains invalid resolver definitions', +}; +// Depends on pipelineFunctionConfigMap +export const pipelineFunctions = { + oneOf: [ + { + $ref: '#/definitions/pipelineFunctionConfigMap', + }, + { + type: 'array', + items: { + $ref: '#/definitions/pipelineFunctionConfigMap', + }, + }, + ], + errorMessage: 'contains invalid pipeline function definitions', +}; +// Depends on stringOrIntrinsicFunction +export const esbuild = { + oneOf: [ + { + type: 'object', + }, + { const: false }, + ], + errorMessage: 'must be an esbuild config object or false', +}; +export const apiId = { $ref: '#/definitions/stringOrIntrinsicFunction' }; From 9a87a187571a6c62fb3c762fc40d309d42d1f2ad Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 18:28:14 +0100 Subject: [PATCH 14/30] fix circular definition --- src/validation.ts | 148 +++++++++++++++------------------- src/validation/definitions.ts | 41 +++++----- 2 files changed, 86 insertions(+), 103 deletions(-) diff --git a/src/validation.ts b/src/validation.ts index 6dd0dcba..8db05fce 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -2,101 +2,52 @@ import Ajv from 'ajv'; import ajvErrors from 'ajv-errors'; import ajvMergePatch from 'ajv-merge-patch'; import addFormats from 'ajv-formats'; -import * as definitions from './validation/definitions'; -import * as properties from './validation/properties'; +import * as def from './validation/definitions'; +import * as prop from './validation/properties'; -export const fullAppSyncSchema = { +const commonProperties = { + substitutions: prop.substitutions, + dataSources: prop.dataSources, + resolvers: prop.resolvers, + pipelineFunctions: prop.pipelineFunctions, +}; + +export const sharedAppSyncSchema = { type: 'object', - definitions: { - stringOrIntrinsicFunction: definitions.stringOrIntrinsicFunction, - lambdaFunctionConfig: definitions.lambdaFunctionConfig, - auth: definitions.auth, - cognitoAuth: definitions.cognitoAuth, - lambdaAuth: definitions.lambdaAuth, - oidcAuth: definitions.oidcAuth, - iamAuth: definitions.iamAuth, - apiKeyAuth: definitions.apiKeyAuth, - visibilityConfig: definitions.visibilityConfig, - wafRule: definitions.wafRule, - customWafRule: definitions.customWafRule, - substitutions: definitions.substitutions, - environment: definitions.environment, - dataSource: definitions.dataSource, - resolverConfig: definitions.resolverConfig, - resolverConfigMap: definitions.resolverConfigMap, - pipelineFunctionConfig: definitions.pipelineFunctionConfig, - pipelineFunction: definitions.pipelineFunction, - pipelineFunctionConfigMap: definitions.pipelineFunctionConfigMap, - resolverCachingConfig: definitions.resolverCachingConfig, - syncConfig: definitions.syncConfig, - iamRoleStatements: definitions.iamRoleStatements, - dataSourceConfig: definitions.dataSourceConfig, - dataSourceHttpConfig: definitions.dataSourceHttpConfig, - dataSourceDynamoDb: definitions.dataSourceDynamoDb, - datasourceRelationalDbConfig: definitions.datasourceRelationalDbConfig, - datasourceLambdaConfig: definitions.datasourceLambdaConfig, - datasourceEsConfig: definitions.datasourceEsConfig, - datasourceEventBridgeConfig: definitions.datasourceEventBridgeConfig, - }, properties: { - name: properties.name, - authentication: properties.authentication, - schema: properties.schema, - domain: properties.domain, - xrayEnabled: properties.xrayEnabled, - visibility: properties.visibility, - introspection: properties.introspection, - queryDepthLimit: properties.queryDepthLimit, - resolverCountLimit: properties.resolverCountLimit, - substitutions: properties.substitutions, - environment: properties.environment, - waf: properties.waf, - tags: properties.tags, - caching: properties.caching, - additionalAuthentications: properties.additionalAuthentications, - apiKeys: properties.apiKeys, - logging: properties.logging, - dataSources: properties.dataSources, - resolvers: properties.resolvers, - pipelineFunctions: properties.pipelineFunctions, - esbuild: properties.esbuild, + ...commonProperties, + apiId: { type: 'string' }, // properties.apiId, // TODO: Handle intrinsic function }, - required: ['name'], + required: ['apiId'], additionalProperties: { not: true, errorMessage: 'invalid (unknown) property', }, }; -export const sharedAppSyncSchema = { + +export const fullAppSyncSchema = { type: 'object', - definitions: { - stringOrIntrinsicFunction: definitions.stringOrIntrinsicFunction, - lambdaFunctionConfig: definitions.lambdaFunctionConfig, - substitutions: definitions.substitutions, - dataSource: definitions.dataSource, - resolverConfig: definitions.resolverConfig, - resolverConfigMap: definitions.resolverConfigMap, - pipelineFunctionConfig: definitions.pipelineFunctionConfig, - pipelineFunction: definitions.pipelineFunction, - pipelineFunctionConfigMap: definitions.pipelineFunctionConfigMap, - resolverCachingConfig: definitions.resolverCachingConfig, - iamRoleStatements: definitions.iamRoleStatements, - dataSourceConfig: definitions.dataSourceConfig, - dataSourceHttpConfig: definitions.dataSourceHttpConfig, - dataSourceDynamoDb: definitions.dataSourceDynamoDb, - datasourceRelationalDbConfig: definitions.datasourceRelationalDbConfig, - datasourceLambdaConfig: definitions.datasourceLambdaConfig, - datasourceEsConfig: definitions.datasourceEsConfig, - datasourceEventBridgeConfig: definitions.datasourceEventBridgeConfig, - }, properties: { - substitutions: properties.substitutions, - dataSources: properties.dataSources, - resolvers: properties.resolvers, - pipelineFunctions: properties.pipelineFunctions, - apiId: { type: 'string' }, // properties.apiId, // TODO: Handle intrinsic function + ...commonProperties, + name: prop.name, + authentication: prop.authentication, + schema: prop.schema, + domain: prop.domain, + xrayEnabled: prop.xrayEnabled, + visibility: prop.visibility, + introspection: prop.introspection, + queryDepthLimit: prop.queryDepthLimit, + resolverCountLimit: prop.resolverCountLimit, + environment: prop.environment, + waf: prop.waf, + tags: prop.tags, + caching: prop.caching, + additionalAuthentications: prop.additionalAuthentications, + apiKeys: prop.apiKeys, + logging: prop.logging, + esbuild: prop.esbuild, }, - required: ['apiId'], + required: ['name'], additionalProperties: { not: true, errorMessage: 'invalid (unknown) property', @@ -104,6 +55,37 @@ export const sharedAppSyncSchema = { }; const appSyncSchema = { + definitions: { + stringOrIntrinsicFunction: def.stringOrIntrinsicFunction, + substitutions: def.substitutions, + lambdaFunctionConfig: def.lambdaFunctionConfig, + dataSource: def.dataSource, + resolverConfig: def.resolverConfig, + resolverConfigMap: def.resolverConfigMap, + pipelineFunctionConfig: def.pipelineFunctionConfig, + pipelineFunction: def.pipelineFunction, + pipelineFunctionConfigMap: def.pipelineFunctionConfigMap, + resolverCachingConfig: def.resolverCachingConfig, + iamRoleStatements: def.iamRoleStatements, + dataSourceConfig: def.dataSourceConfig, + dataSourceHttpConfig: def.dataSourceHttpConfig, + dataSourceDynamoDb: def.dataSourceDynamoDb, + datasourceRelationalDbConfig: def.datasourceRelationalDbConfig, + datasourceLambdaConfig: def.datasourceLambdaConfig, + datasourceEsConfig: def.datasourceEsConfig, + datasourceEventBridgeConfig: def.datasourceEventBridgeConfig, + auth: def.auth, + cognitoAuth: def.cognitoAuth, + lambdaAuth: def.lambdaAuth, + oidcAuth: def.oidcAuth, + iamAuth: def.iamAuth, + apiKeyAuth: def.apiKeyAuth, + visibilityConfig: def.visibilityConfig, + wafRule: def.wafRule, + customWafRule: def.customWafRule, + environment: def.environment, + syncConfig: def.syncConfig, + }, oneOf: [sharedAppSyncSchema, fullAppSyncSchema], }; diff --git a/src/validation/definitions.ts b/src/validation/definitions.ts index 09bb375b..a8731a44 100644 --- a/src/validation/definitions.ts +++ b/src/validation/definitions.ts @@ -1,3 +1,24 @@ +export const stringOrIntrinsicFunction = { + oneOf: [ + { type: 'string' }, + { + type: 'object', + required: [], + additionalProperties: true, + }, + ], + errorMessage: 'must be a string or a CloudFormation intrinsic function', +}; +// Depends on stringOrIntrinsicFunction +export const substitutions = { + type: 'object', + additionalProperties: { + $ref: '#/definitions/stringOrIntrinsicFunction', + }, + required: [], + errorMessage: 'must be a valid substitutions definition', +}; + export const AUTH_TYPES = [ 'AMAZON_COGNITO_USER_POOLS', 'AWS_LAMBDA', @@ -16,17 +37,6 @@ export const DATASOURCE_TYPES = [ 'AMAZON_EVENTBRIDGE', ] as const; -export const stringOrIntrinsicFunction = { - oneOf: [ - { type: 'string' }, - { - type: 'object', - required: [], - additionalProperties: true, - }, - ], - errorMessage: 'must be a string or a CloudFormation intrinsic function', -}; // Depends on stringOrIntrinsicFunction export const lambdaFunctionConfig = { oneOf: [ @@ -253,15 +263,6 @@ export const customWafRule = { required: ['name', 'statement'], }; -// Depends on stringOrIntrinsicFunction -export const substitutions = { - type: 'object', - additionalProperties: { - $ref: '#/definitions/stringOrIntrinsicFunction', - }, - required: [], - errorMessage: 'must be a valid substitutions definition', -}; // Depends on stringOrIntrinsicFunction export const environment = { type: 'object', From 6bfb87e6e2d1bd740736b2d9559311e378accceb Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 21:03:49 +0100 Subject: [PATCH 15/30] fix tests --- src/__tests__/getAppSyncConfig.test.ts | 4 +- .../__snapshots__/base.test.ts.snap | 5 +- src/__tests__/validation/base.test.ts | 16 ++- src/validation.ts | 105 +++++++++++------- 4 files changed, 84 insertions(+), 46 deletions(-) diff --git a/src/__tests__/getAppSyncConfig.test.ts b/src/__tests__/getAppSyncConfig.test.ts index 53c81100..8b4ab26a 100644 --- a/src/__tests__/getAppSyncConfig.test.ts +++ b/src/__tests__/getAppSyncConfig.test.ts @@ -39,7 +39,7 @@ describe('Api Keys', () => { ...basicConfig, authentication: { type: 'AWS_IAM' }, }); - const apiKeys = 'apiKeys' in config ? config.schema : undefined; + const apiKeys = 'apiKeys' in config ? config.apiKeys : undefined; expect(apiKeys).toBeUndefined(); }); @@ -59,7 +59,7 @@ describe('Api Keys', () => { 'InlineKey', ], }); - const apiKeys = 'apiKeys' in config ? config.schema : undefined; + const apiKeys = 'apiKeys' in config ? config.apiKeys : undefined; expect(apiKeys).toMatchInlineSnapshot(` Object { diff --git a/src/__tests__/validation/__snapshots__/base.test.ts.snap b/src/__tests__/validation/__snapshots__/base.test.ts.snap index 7afdb44d..a585224b 100644 --- a/src/__tests__/validation/__snapshots__/base.test.ts.snap +++ b/src/__tests__/validation/__snapshots__/base.test.ts.snap @@ -52,8 +52,7 @@ exports[`Valdiation Waf Invalid should validate a Throttle limit 1`] = ` `; exports[`Valdiation should validate 1`] = ` -": must have required property 'name' -: must have required property 'authentication' +": must have required property 'authentication' /unknownPorp: invalid (unknown) property /xrayEnabled: must be boolean /visibility: must be \\"GLOBAL\\" or \\"PRIVATE\\" @@ -68,3 +67,5 @@ exports[`Valdiation should validate 2`] = ` "/queryDepthLimit: must be <= 75 /resolverCountLimit: must be <= 1000" `; + +exports[`Valdiation should validate 3`] = `"Invalid configuration: must contain either \\\"apiId\\\" or \\\"name\\\""`; diff --git a/src/__tests__/validation/base.test.ts b/src/__tests__/validation/base.test.ts index 5824bc2d..bddd5bb9 100644 --- a/src/__tests__/validation/base.test.ts +++ b/src/__tests__/validation/base.test.ts @@ -29,6 +29,7 @@ describe('Valdiation', () => { expect(function () { validateConfig({ + name: 'FOO', visibility: 'FOO', introspection: 10, queryDepthLimit: 'foo', @@ -47,6 +48,19 @@ describe('Valdiation', () => { resolverCountLimit: 1001, }); }).toThrowErrorMatchingSnapshot(); + + expect(function () { + validateConfig({ + visibility: 'FOO', + introspection: 10, + queryDepthLimit: 'foo', + resolverCountLimit: 'bar', + xrayEnabled: 'BAR', + unknownPorp: 'foo', + esbuild: 'bad', + environment: 'Bad', + }); + }).toThrowErrorMatchingSnapshot(); }); describe('Log', () => { @@ -69,7 +83,7 @@ describe('Valdiation', () => { level: 'ALL', retentionInDays: 14, excludeVerboseContent: true, - loggingRoleArn: { Ref: 'MyLogGorupArn' }, + // loggingRoleArn: { Ref: 'MyLogGorupArn' }, // TODO : why was it only in the tests ? }, } as AppSyncConfig, }, diff --git a/src/validation.ts b/src/validation.ts index 8db05fce..e527584d 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -1,4 +1,4 @@ -import Ajv from 'ajv'; +import Ajv, { type ValidateFunction } from 'ajv'; import ajvErrors from 'ajv-errors'; import ajvMergePatch from 'ajv-merge-patch'; import addFormats from 'ajv-formats'; @@ -12,8 +12,41 @@ const commonProperties = { pipelineFunctions: prop.pipelineFunctions, }; +const definitions = { + stringOrIntrinsicFunction: def.stringOrIntrinsicFunction, + substitutions: def.substitutions, + lambdaFunctionConfig: def.lambdaFunctionConfig, + dataSource: def.dataSource, + resolverConfig: def.resolverConfig, + resolverConfigMap: def.resolverConfigMap, + pipelineFunctionConfig: def.pipelineFunctionConfig, + pipelineFunction: def.pipelineFunction, + pipelineFunctionConfigMap: def.pipelineFunctionConfigMap, + resolverCachingConfig: def.resolverCachingConfig, + iamRoleStatements: def.iamRoleStatements, + dataSourceConfig: def.dataSourceConfig, + dataSourceHttpConfig: def.dataSourceHttpConfig, + dataSourceDynamoDb: def.dataSourceDynamoDb, + datasourceRelationalDbConfig: def.datasourceRelationalDbConfig, + datasourceLambdaConfig: def.datasourceLambdaConfig, + datasourceEsConfig: def.datasourceEsConfig, + datasourceEventBridgeConfig: def.datasourceEventBridgeConfig, + auth: def.auth, + cognitoAuth: def.cognitoAuth, + lambdaAuth: def.lambdaAuth, + oidcAuth: def.oidcAuth, + iamAuth: def.iamAuth, + apiKeyAuth: def.apiKeyAuth, + visibilityConfig: def.visibilityConfig, + wafRule: def.wafRule, + customWafRule: def.customWafRule, + environment: def.environment, + syncConfig: def.syncConfig, +}; + export const sharedAppSyncSchema = { type: 'object', + definitions, properties: { ...commonProperties, apiId: { type: 'string' }, // properties.apiId, // TODO: Handle intrinsic function @@ -27,6 +60,7 @@ export const sharedAppSyncSchema = { export const fullAppSyncSchema = { type: 'object', + definitions, properties: { ...commonProperties, name: prop.name, @@ -47,56 +81,45 @@ export const fullAppSyncSchema = { logging: prop.logging, esbuild: prop.esbuild, }, - required: ['name'], + required: ['name', 'authentication'], additionalProperties: { not: true, errorMessage: 'invalid (unknown) property', }, }; -const appSyncSchema = { - definitions: { - stringOrIntrinsicFunction: def.stringOrIntrinsicFunction, - substitutions: def.substitutions, - lambdaFunctionConfig: def.lambdaFunctionConfig, - dataSource: def.dataSource, - resolverConfig: def.resolverConfig, - resolverConfigMap: def.resolverConfigMap, - pipelineFunctionConfig: def.pipelineFunctionConfig, - pipelineFunction: def.pipelineFunction, - pipelineFunctionConfigMap: def.pipelineFunctionConfigMap, - resolverCachingConfig: def.resolverCachingConfig, - iamRoleStatements: def.iamRoleStatements, - dataSourceConfig: def.dataSourceConfig, - dataSourceHttpConfig: def.dataSourceHttpConfig, - dataSourceDynamoDb: def.dataSourceDynamoDb, - datasourceRelationalDbConfig: def.datasourceRelationalDbConfig, - datasourceLambdaConfig: def.datasourceLambdaConfig, - datasourceEsConfig: def.datasourceEsConfig, - datasourceEventBridgeConfig: def.datasourceEventBridgeConfig, - auth: def.auth, - cognitoAuth: def.cognitoAuth, - lambdaAuth: def.lambdaAuth, - oidcAuth: def.oidcAuth, - iamAuth: def.iamAuth, - apiKeyAuth: def.apiKeyAuth, - visibilityConfig: def.visibilityConfig, - wafRule: def.wafRule, - customWafRule: def.customWafRule, - environment: def.environment, - syncConfig: def.syncConfig, - }, - oneOf: [sharedAppSyncSchema, fullAppSyncSchema], +// const appSyncSchema = { + +// oneOf: [sharedAppSyncSchema, fullAppSyncSchema], +// }; + +const createValidator = (schema: object) => { + const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); + ajvMergePatch(ajv); + ajvErrors(ajv); + addFormats(ajv); + return ajv.compile(schema); }; -const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); -ajvMergePatch(ajv); -ajvErrors(ajv); -addFormats(ajv); +const sharedValidator = createValidator(sharedAppSyncSchema); +const fullValidator = createValidator(fullAppSyncSchema); -const validator = ajv.compile(appSyncSchema); export const validateConfig = (data: Record) => { - const isValid = validator(data); + let isValid: boolean; + let validator: ValidateFunction; + + if ('apiId' in data) { + isValid = sharedValidator(data); + validator = sharedValidator; + } else if ('name' in data) { + isValid = fullValidator(data); + validator = fullValidator; + } else { + throw new Error( + 'Invalid configuration: must contain either "apiId" or "name"', + ); + } + if (isValid === false && validator.errors) { throw new AppSyncValidationError( validator.errors From 6374e634523656a9ad265e0b991eaee24021f7e4 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 18 Nov 2024 21:15:01 +0100 Subject: [PATCH 16/30] cleanup --- src/resources/Api.ts | 2 +- src/types/plugin.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/resources/Api.ts b/src/resources/Api.ts index 1d68b46c..5ac60df5 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -224,7 +224,7 @@ export class Api { compileSchema() { if (isSharedApiConfig(this.config)) return {}; - if (!this.config.schema) return {}; // is this the expected behaviour ? + if (!this.config.schema) return {}; const schema = new Schema(this, this.config.schema); return schema.compile(); diff --git a/src/types/plugin.ts b/src/types/plugin.ts index ecbc1b6e..c56d7827 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -20,7 +20,6 @@ import type { } from './common'; export * from './common'; -// TODO: The same should happen in the validation json schema. export type BaseAppSyncConfig = { dataSources: Record; resolvers: Record; From a203d3ac32b1c40d71af113ad46317b4e72ab9f6 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 02:44:40 +0100 Subject: [PATCH 17/30] build tsconf for a newer version --- tsconfig.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 84c86304..969ebd28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "target": "es5", + "target": "es2018", "module": "commonjs", "moduleResolution": "node", "sourceMap": true, @@ -11,8 +11,10 @@ "skipLibCheck": true, "outDir": "lib", "noImplicitAny": false, - "declaration": true + "declaration": true, + "lib": ["es2018"], + "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "src/__tests__"] -} +} \ No newline at end of file From 74e573746bede54f07e0f90707b5b808f313e455 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 04:11:45 +0100 Subject: [PATCH 18/30] build to module --- serverless-appsync-plugin-0.0.0-development.tgz | Bin 0 -> 65607 bytes src/index.ts | 2 +- tsconfig.json | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 serverless-appsync-plugin-0.0.0-development.tgz diff --git a/serverless-appsync-plugin-0.0.0-development.tgz b/serverless-appsync-plugin-0.0.0-development.tgz new file mode 100644 index 0000000000000000000000000000000000000000..6abc7dc847d0b1ce5af565275052e1f360f79c91 GIT binary patch literal 65607 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 apJ=+011|bLZX@151Bqz5KtM1cLH-Z7e+vTu literal 0 HcmV?d00001 diff --git a/src/index.ts b/src/index.ts index 5fa12757..306bbb8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,7 +64,7 @@ import { ListCertificatesResponse, } from 'aws-sdk/clients/acm'; import terminalLink from 'terminal-link'; -import { AppSyncConfig, isSharedApiConfig } from './types/plugin'; +import { type AppSyncConfig, isSharedApiConfig } from './types/plugin'; const CONSOLE_BASE_URL = 'https://console.aws.amazon.com'; diff --git a/tsconfig.json b/tsconfig.json index 969ebd28..b22c3b65 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "target": "es2018", - "module": "commonjs", + "module": "ESNext", "moduleResolution": "node", "sourceMap": true, "strict": true, @@ -12,7 +12,6 @@ "outDir": "lib", "noImplicitAny": false, "declaration": true, - "lib": ["es2018"], "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts"], From 531daebd551c5424020f4728ef6deba2791993cf Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 09:31:52 +0100 Subject: [PATCH 19/30] switched from commonjs to esmodule for sls v4 --- package-lock.json | 12 +++++++++- package.json | 3 ++- ...rless-appsync-plugin-0.0.0-development.tgz | Bin 65607 -> 0 bytes src/__tests__/basicConfig.ts | 2 +- src/__tests__/getAppSyncConfig.test.ts | 10 ++++---- src/__tests__/given.ts | 4 ++-- src/__tests__/utils.ts | 4 ++-- src/__tests__/waf.test.ts | 10 ++++---- src/getAppSyncConfig.ts | 16 +++++++++---- src/index.ts | 14 +++++------ src/resources/Api.ts | 22 ++++++++---------- src/resources/DataSource.ts | 10 ++++---- src/resources/JsResolver.ts | 6 ++--- src/resources/MappingTemplate.ts | 6 ++--- src/resources/Naming.ts | 2 +- src/resources/PipelineFunction.ts | 14 +++++------ src/resources/Resolver.ts | 14 +++++------ src/resources/Schema.ts | 10 ++++---- src/resources/SyncConfig.ts | 4 ++-- src/resources/Waf.ts | 10 ++++---- src/types/index.ts | 4 ++-- src/types/plugin.ts | 4 ++-- src/utils.ts | 4 ++-- src/validation.ts | 4 ++-- src/validation/properties.ts | 2 +- 25 files changed, 103 insertions(+), 88 deletions(-) delete mode 100644 serverless-appsync-plugin-0.0.0-development.tgz diff --git a/package-lock.json b/package-lock.json index 06656a6f..0bbb6c37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "esbuild": "^0.17.11", "globby": "^11.1.0", "graphql": "^16.6.0", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "luxon": "^2.5.0", "open": "^8.4.0", "terminal-link": "^2.1.1" @@ -8552,6 +8552,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", @@ -22248,6 +22253,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", diff --git a/package.json b/package.json index 3b3f0e4b..a93e6445 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "serverless-appsync-plugin", "version": "0.0.0-development", + "type": "module", "description": "AWS AppSync support for the Serverless Framework", "main": "lib/index.js", "types": "lib/types/index.d.ts", @@ -48,7 +49,7 @@ "esbuild": "^0.17.11", "globby": "^11.1.0", "graphql": "^16.6.0", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "luxon": "^2.5.0", "open": "^8.4.0", "terminal-link": "^2.1.1" diff --git a/serverless-appsync-plugin-0.0.0-development.tgz b/serverless-appsync-plugin-0.0.0-development.tgz deleted file mode 100644 index 6abc7dc847d0b1ce5af565275052e1f360f79c91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65607 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 apJ=+011|bLZX@151Bqz5KtM1cLH-Z7e+vTu diff --git a/src/__tests__/basicConfig.ts b/src/__tests__/basicConfig.ts index 7d8692ff..16fc7710 100644 --- a/src/__tests__/basicConfig.ts +++ b/src/__tests__/basicConfig.ts @@ -1,4 +1,4 @@ -import { AppSyncConfig } from '../types'; +import { AppSyncConfig } from '../types/index.js'; export const basicConfig: AppSyncConfig = { name: 'My Api', diff --git a/src/__tests__/getAppSyncConfig.test.ts b/src/__tests__/getAppSyncConfig.test.ts index 8b4ab26a..0b444b10 100644 --- a/src/__tests__/getAppSyncConfig.test.ts +++ b/src/__tests__/getAppSyncConfig.test.ts @@ -1,7 +1,7 @@ -import { pick } from 'lodash'; -import { getAppSyncConfig } from '../getAppSyncConfig'; -import { basicConfig } from './basicConfig'; -import { ResolverConfig } from '../types'; +import { pick } from 'lodash-es'; +import { getAppSyncConfig } from '../getAppSyncConfig.js'; +import { basicConfig } from './basicConfig.js'; +import { ResolverConfig } from '../types/index.js'; test('returns basic config', async () => { expect(getAppSyncConfig(basicConfig)).toMatchSnapshot(); @@ -235,7 +235,7 @@ describe('Resolvers', () => { field: 'getUsers', }, }, - ] as Record[], + ] satisfies Record[], }); expect(config.resolvers).toMatchSnapshot(); }); diff --git a/src/__tests__/given.ts b/src/__tests__/given.ts index 6fbed2d6..2b5c51d5 100644 --- a/src/__tests__/given.ts +++ b/src/__tests__/given.ts @@ -1,7 +1,7 @@ -import { set } from 'lodash'; +import { set } from 'lodash-es'; import Serverless from 'serverless/lib/serverless'; import AwsProvider from 'serverless/lib/plugins/aws/provider.js'; -import { AppSyncConfig } from '../types/plugin'; +import { AppSyncConfig } from '../types/plugin.js'; import ServerlessAppsyncPlugin from '..'; export const createServerless = (): Serverless => { diff --git a/src/__tests__/utils.ts b/src/__tests__/utils.ts index deeaebce..bf015efc 100644 --- a/src/__tests__/utils.ts +++ b/src/__tests__/utils.ts @@ -1,7 +1,7 @@ import runServerlessFixtureEngine from '@serverless/test/setup-run-serverless-fixtures-engine'; -import { merge } from 'lodash'; +import { merge } from 'lodash-es'; import path from 'path'; -import Serverless from 'serverless/lib/Serverless'; +import Serverless from 'serverless'; type RunSlsOptions = { fixture: 'appsync'; diff --git a/src/__tests__/waf.test.ts b/src/__tests__/waf.test.ts index 3fb2616a..deb15647 100644 --- a/src/__tests__/waf.test.ts +++ b/src/__tests__/waf.test.ts @@ -1,8 +1,8 @@ -import { Api } from '../resources/Api'; -import { ApiKeyConfig, WafRule } from '../types/plugin'; -import { each } from 'lodash'; -import { Waf } from '../resources/Waf'; -import * as given from './given'; +import { Api } from '../resources/Api.js'; +import { ApiKeyConfig, WafRule } from '../types/plugin.js'; +import { each } from 'lodash-es'; +import { Waf } from '../resources/Waf.js'; +import * as given from './given.js'; const plugin = given.plugin(); diff --git a/src/getAppSyncConfig.ts b/src/getAppSyncConfig.ts index f343a3d3..44b7a46d 100644 --- a/src/getAppSyncConfig.ts +++ b/src/getAppSyncConfig.ts @@ -1,4 +1,10 @@ -import { AppSyncConfig, isSharedApiConfig } from './types'; +import { + AppSyncConfig, + isSharedApiConfig, + PipelineResolverConfig, + UnitResolverConfig, +} from './types/index.js'; + import type { ApiKeyConfig, AppSyncConfig as PluginAppSyncConfig, @@ -8,8 +14,8 @@ import type { BaseAppSyncConfig, SharedAppSyncConfig, FullAppSyncConfig, -} from './types/plugin'; -import { forEach, merge } from 'lodash'; +} from './types/plugin.js'; +import { forEach, merge } from 'lodash-es'; const flattenMaps = ( input?: Record | Record[], @@ -23,13 +29,13 @@ const flattenMaps = ( export const isUnitResolver = (resolver: { kind?: 'UNIT' | 'PIPELINE'; -}): resolver is { kind: 'UNIT' } => { +}): resolver is UnitResolverConfig => { return resolver.kind === 'UNIT'; }; export const isPipelineResolver = (resolver: { kind?: 'UNIT' | 'PIPELINE'; -}): resolver is { kind: 'PIPELINE' } => { +}): resolver is PipelineResolverConfig => { return !resolver.kind || resolver.kind === 'PIPELINE'; }; diff --git a/src/index.ts b/src/index.ts index 306bbb8f..7a600e90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import Serverless from 'serverless/lib/Serverless'; import Provider from 'serverless/lib/plugins/aws/provider.js'; -import { forEach, last, merge } from 'lodash'; -import { getAppSyncConfig } from './getAppSyncConfig'; +import { forEach, last, merge } from 'lodash-es'; +import { getAppSyncConfig } from './getAppSyncConfig.js'; import { GraphQLError } from 'graphql'; import { DateTime } from 'luxon'; import chalk from 'chalk'; @@ -41,16 +41,16 @@ import { FilterLogEventsResponse, FilterLogEventsRequest, } from 'aws-sdk/clients/cloudwatchlogs'; -import { AppSyncValidationError, validateConfig } from './validation'; +import { AppSyncValidationError, validateConfig } from './validation.js'; import { confirmAction, getHostedZoneName, getWildCardDomainName, parseDateTimeOrDuration, wait, -} from './utils'; -import { Api } from './resources/Api'; -import { Naming } from './resources/Naming'; +} from './utils.js'; +import { Api } from './resources/Api.js'; +import { Naming } from './resources/Naming.js'; import { ChangeResourceRecordSetsRequest, ChangeResourceRecordSetsResponse, @@ -64,7 +64,7 @@ import { ListCertificatesResponse, } from 'aws-sdk/clients/acm'; import terminalLink from 'terminal-link'; -import { type AppSyncConfig, isSharedApiConfig } from './types/plugin'; +import { AppSyncConfig, isSharedApiConfig } from './types/plugin.js'; const CONSOLE_BASE_URL = 'https://console.aws.amazon.com'; diff --git a/src/resources/Api.ts b/src/resources/Api.ts index 5ac60df5..67aad1b3 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -1,10 +1,10 @@ import ServerlessAppsyncPlugin from '..'; -import { forEach, isEmpty, merge, set } from 'lodash'; +import { forEach, isEmpty, merge, set } from 'lodash-es'; import { CfnResource, CfnResources, IntrinsicFunction, -} from '../types/cloudFormation'; +} from '../types/cloudFormation.js'; import { ApiKeyConfig, AppSyncConfig, @@ -16,18 +16,16 @@ import { LambdaConfig, OidcAuth, ResolverConfig, - FullAppSyncConfig, - SharedAppSyncConfig, isSharedApiConfig, -} from '../types/plugin'; -import { getHostedZoneName, parseDuration } from '../utils'; +} from '../types/plugin.js'; +import { getHostedZoneName, parseDuration } from '../utils.js'; import { DateTime, Duration } from 'luxon'; -import { Naming } from './Naming'; -import { DataSource } from './DataSource'; -import { Resolver } from './Resolver'; -import { PipelineFunction } from './PipelineFunction'; -import { Schema } from './Schema'; -import { Waf } from './Waf'; +import { Naming } from './Naming.js'; +import { DataSource } from './DataSource.js'; +import { Resolver } from './Resolver.js'; +import { PipelineFunction } from './PipelineFunction.js'; +import { Schema } from './Schema.js'; +import { Waf } from './Waf.js'; import { log } from '@serverless/utils/log'; export class Api { diff --git a/src/resources/DataSource.ts b/src/resources/DataSource.ts index 70d4e177..86dbbc75 100644 --- a/src/resources/DataSource.ts +++ b/src/resources/DataSource.ts @@ -1,9 +1,9 @@ -import { merge } from 'lodash'; +import { merge } from 'lodash-es'; import { CfnDataSource, CfnResources, IntrinsicFunction, -} from '../types/cloudFormation'; +} from '../types/cloudFormation.js'; import { DataSourceConfig, DsDynamoDBConfig, @@ -12,9 +12,9 @@ import { DsRelationalDbConfig, IamStatement, DsEventBridgeConfig, -} from '../types/plugin'; -import { Api } from './Api'; -import { Naming } from './Naming'; +} from '../types/plugin.js'; +import { Api } from './Api.js'; +import { Naming } from './Naming.js'; export class DataSource { constructor(private api: Api, private config: DataSourceConfig) {} diff --git a/src/resources/JsResolver.ts b/src/resources/JsResolver.ts index f6a5ba88..ffd669f1 100644 --- a/src/resources/JsResolver.ts +++ b/src/resources/JsResolver.ts @@ -1,7 +1,7 @@ -import { IntrinsicFunction } from '../types/cloudFormation'; +import { IntrinsicFunction } from '../types/cloudFormation.js'; import fs from 'fs'; -import { isSharedApiConfig, Substitutions } from '../types/plugin'; -import { Api } from './Api'; +import { isSharedApiConfig, Substitutions } from '../types/plugin.js'; +import { Api } from './Api.js'; import { buildSync } from 'esbuild'; type JsResolverConfig = { diff --git a/src/resources/MappingTemplate.ts b/src/resources/MappingTemplate.ts index 36bc78c6..8f3fc408 100644 --- a/src/resources/MappingTemplate.ts +++ b/src/resources/MappingTemplate.ts @@ -1,7 +1,7 @@ -import { IntrinsicFunction } from '../types/cloudFormation'; +import { IntrinsicFunction } from '../types/cloudFormation.js'; import fs from 'fs'; -import { Substitutions } from '../types/plugin'; -import { Api } from './Api'; +import { Substitutions } from '../types/plugin.js'; +import { Api } from './Api.js'; type MappingTemplateConfig = { path: string; diff --git a/src/resources/Naming.ts b/src/resources/Naming.ts index 0b8c5aa0..adf4178d 100644 --- a/src/resources/Naming.ts +++ b/src/resources/Naming.ts @@ -2,7 +2,7 @@ import { DataSourceConfig, PipelineFunctionConfig, ResolverConfig, -} from '../types/plugin'; +} from '../types/plugin.js'; export class Naming { constructor(private apiName: string) {} diff --git a/src/resources/PipelineFunction.ts b/src/resources/PipelineFunction.ts index d56d123f..d5fb4d1e 100644 --- a/src/resources/PipelineFunction.ts +++ b/src/resources/PipelineFunction.ts @@ -2,14 +2,14 @@ import { CfnFunctionResolver, CfnResources, IntrinsicFunction, -} from '../types/cloudFormation'; -import { PipelineFunctionConfig } from '../types/plugin'; -import { Api } from './Api'; +} from '../types/cloudFormation.js'; +import { PipelineFunctionConfig } from '../types/plugin.js'; +import { Api } from './Api.js'; import path from 'path'; -import { MappingTemplate } from './MappingTemplate'; -import { SyncConfig } from './SyncConfig'; -import { JsResolver } from './JsResolver'; -import { Naming } from './Naming'; +import { MappingTemplate } from './MappingTemplate.js'; +import { SyncConfig } from './SyncConfig.js'; +import { JsResolver } from './JsResolver.js'; +import { Naming } from './Naming.js'; export class PipelineFunction { constructor(private api: Api, private config: PipelineFunctionConfig) {} diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 0d78a901..cc74fe1c 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -3,14 +3,14 @@ import { CfnResource, CfnResources, IntrinsicFunction, -} from '../types/cloudFormation'; -import { isSharedApiConfig, ResolverConfig } from '../types/plugin'; -import { Api } from './Api'; +} from '../types/cloudFormation.js'; +import { isSharedApiConfig, ResolverConfig } from '../types/plugin.js'; +import { Api } from './Api.js'; import path from 'path'; -import { MappingTemplate } from './MappingTemplate'; -import { SyncConfig } from './SyncConfig'; -import { JsResolver } from './JsResolver'; -import { Naming } from './Naming'; +import { MappingTemplate } from './MappingTemplate.js'; +import { SyncConfig } from './SyncConfig.js'; +import { JsResolver } from './JsResolver.js'; +import { Naming } from './Naming.js'; // A decent default for pipeline JS resolvers const DEFAULT_JS_RESOLVERS = ` diff --git a/src/resources/Schema.ts b/src/resources/Schema.ts index 2013394c..e2759dc2 100644 --- a/src/resources/Schema.ts +++ b/src/resources/Schema.ts @@ -1,13 +1,13 @@ import globby from 'globby'; import fs from 'fs'; import path from 'path'; -import { CfnResources } from '../types/cloudFormation'; -import { Api } from './Api'; -import { flatten } from 'lodash'; +import { CfnResources } from '../types/cloudFormation.js'; +import { Api } from './Api.js'; +import { flatten } from 'lodash-es'; import { parse, print } from 'graphql'; -import { validateSDL } from 'graphql/validation/validate'; +import { validateSDL } from 'graphql/validation/validate.js'; import { mergeTypeDefs } from '@graphql-tools/merge'; -import { isSharedApiConfig } from '../types/plugin'; +import { isSharedApiConfig } from '../types/plugin.js'; const AWS_TYPES = ` directive @aws_iam on FIELD_DEFINITION | OBJECT diff --git a/src/resources/SyncConfig.ts b/src/resources/SyncConfig.ts index 65d4bcab..54ef5526 100644 --- a/src/resources/SyncConfig.ts +++ b/src/resources/SyncConfig.ts @@ -2,8 +2,8 @@ import { isSharedApiConfig, PipelineFunctionConfig, ResolverConfig, -} from '../types/plugin'; -import { Api } from './Api'; +} from '../types/plugin.js'; +import { Api } from './Api.js'; export class SyncConfig { constructor( diff --git a/src/resources/Waf.ts b/src/resources/Waf.ts index 85a71276..5d08ba4d 100644 --- a/src/resources/Waf.ts +++ b/src/resources/Waf.ts @@ -1,10 +1,10 @@ -import { isEmpty, reduce } from 'lodash'; +import { isEmpty, reduce } from 'lodash-es'; import { CfnResources, CfnWafAction, CfnWafRule, CfnWafRuleStatement, -} from '../types/cloudFormation'; +} from '../types/cloudFormation.js'; import { ApiKeyConfig, isSharedApiConfig, @@ -13,9 +13,9 @@ import { WafRuleAction, WafRuleDisableIntrospection, WafThrottleConfig, -} from '../types/plugin'; -import { Api } from './Api'; -import { toCfnKeys } from '../utils'; +} from '../types/plugin.js'; +import { Api } from './Api.js'; +import { toCfnKeys } from '../utils.js'; export class Waf { constructor(private api: Api, private config: WafConfig) {} diff --git a/src/types/index.ts b/src/types/index.ts index e5f5b3b2..2c30f17e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,8 +17,8 @@ import type { DsRelationalDbConfig, SyncConfig, EnvironmentVariables, -} from './common'; -export * from './common'; +} from './common.js'; +export * from './common.js'; type BaseAppSyncConfig = { dataSources: diff --git a/src/types/plugin.ts b/src/types/plugin.ts index c56d7827..cb9c925a 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -17,8 +17,8 @@ import type { DsRelationalDbConfig, SyncConfig, EnvironmentVariables, -} from './common'; -export * from './common'; +} from './common.js'; +export * from './common.js'; export type BaseAppSyncConfig = { dataSources: Record; diff --git a/src/utils.ts b/src/utils.ts index 5c6e110f..e688655f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ -import { upperFirst, transform, values } from 'lodash'; -import { TransformKeysToCfnCase } from './typeHelpers'; +import { upperFirst, transform, values } from 'lodash-es'; +import { TransformKeysToCfnCase } from './typeHelpers.js'; import { DateTime, Duration } from 'luxon'; import { promisify } from 'util'; import * as readline from 'readline'; diff --git a/src/validation.ts b/src/validation.ts index e527584d..1722fec5 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -2,8 +2,8 @@ import Ajv, { type ValidateFunction } from 'ajv'; import ajvErrors from 'ajv-errors'; import ajvMergePatch from 'ajv-merge-patch'; import addFormats from 'ajv-formats'; -import * as def from './validation/definitions'; -import * as prop from './validation/properties'; +import * as def from './validation/definitions.js'; +import * as prop from './validation/properties.js'; const commonProperties = { substitutions: prop.substitutions, diff --git a/src/validation/properties.ts b/src/validation/properties.ts index 273c2176..93f39a1a 100644 --- a/src/validation/properties.ts +++ b/src/validation/properties.ts @@ -1,4 +1,4 @@ -import { timeUnits } from '../utils'; +import { timeUnits } from '../utils.js'; export const name = { type: 'string' }; // Depends on auth From 25ae99244f1f90be3dd31d260c04a59f70ddbb6e Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 14:30:05 +0100 Subject: [PATCH 20/30] better errors handeling --- src/index.ts | 2 +- src/resources/Api.ts | 28 ++++++++++++++++++---------- src/resources/DataSource.ts | 10 ++++++---- src/resources/Resolver.ts | 5 ++++- src/resources/Schema.ts | 8 ++++++-- src/resources/SyncConfig.ts | 8 ++++++-- src/resources/Waf.ts | 20 +++++++++++++++----- 7 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7a600e90..2a58fee9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -967,7 +967,7 @@ class ServerlessAppsyncPlugin { displayEndpoints() { if (!this.api?.config || isSharedApiConfig(this.api.config)) { - throw this.serverless.classes.Error( + throw new this.serverless.classes.Error( 'Impossible to display endpoints from a Shared Appsync', ); } diff --git a/src/resources/Api.ts b/src/resources/Api.ts index 67aad1b3..d36ac20a 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -75,7 +75,8 @@ export class Api { compileEndpoint(): CfnResources { // in a class, the type needs to be cheked every time if (isSharedApiConfig(this.config)) return {}; - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); const logicalId = this.naming.getApiLogicalId(); const endpointResource: CfnResource = { @@ -153,7 +154,8 @@ export class Api { ) { return {}; } - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); const logGroupLogicalId = this.naming.getLogGroupLogicalId(); const roleLogicalId = this.naming.getLogGroupRoleLogicalId(); @@ -230,7 +232,8 @@ export class Api { compileCustomDomain(): CfnResources { if (isSharedApiConfig(this.config)) return {}; - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); const { domain } = this.config; if ( @@ -323,7 +326,8 @@ export class Api { compileLambdaAuthorizerPermission(): CfnResources { if (isSharedApiConfig(this.config)) return {}; - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); if (!this.config.authentication) return {}; @@ -357,7 +361,8 @@ export class Api { compileApiKey(config: ApiKeyConfig) { if (isSharedApiConfig(this.config)) return {}; - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); const { name, expiresAt, expiresAfter, description, apiKeyId } = config; @@ -386,7 +391,7 @@ export class Api { expires < DateTime.now().plus({ day: 1 }) || expires > DateTime.now().plus({ years: 365 }) ) { - throw new Error( + throw new this.plugin.serverless.classes.Error( `Api Key ${name} must be valid for a minimum of 1 day and a maximum of 365 days.`, ); } @@ -408,7 +413,8 @@ export class Api { compileCachingResources(): CfnResources { if (isSharedApiConfig(this.config)) return {}; - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); if (!this.config.caching || this.config.caching?.enabled === false) { return {}; @@ -464,7 +470,8 @@ export class Api { if (isSharedApiConfig(this.config) && this.config.apiId) { return this.config.apiId; } - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); const logicalIdGraphQLApi = this.naming.getApiLogicalId(); return { 'Fn::GetAtt': [logicalIdGraphQLApi, 'ApiId'], @@ -504,7 +511,8 @@ export class Api { } getLambdaAuthorizerConfig(auth: LambdaAuth) { - if (!this.naming) throw new Error('Unable to load naming'); + if (!this.naming) + throw new this.plugin.serverless.classes.Error('Unable to load naming'); if (!auth.config) { return; } @@ -565,7 +573,7 @@ export class Api { return this.generateLambdaArn(embededFunctionName); } - throw new Error( + throw new this.plugin.serverless.classes.Error( 'You must specify either `functionArn`, `functionName` or `function` for lambda definitions.', ); } diff --git a/src/resources/DataSource.ts b/src/resources/DataSource.ts index 86dbbc75..01c94d17 100644 --- a/src/resources/DataSource.ts +++ b/src/resources/DataSource.ts @@ -128,7 +128,9 @@ export class DataSource { }); // FIXME: can we validate this and make TS infer mutually eclusive types? if (!endpoint) { - throw new Error('Specify eithe rendpoint or domain'); + throw new this.api.plugin.serverless.classes.Error( + 'Specify eithe rendpoint or domain', + ); } return { AwsRegion: config.config.region || { Ref: 'AWS::Region' }, @@ -205,7 +207,7 @@ export class DataSource { this.config.config.authorizationConfig.authorizationType === 'AWS_IAM' && !this.config.config.iamRoleStatements ) { - throw new Error( + throw new this.api.plugin.serverless.classes.Error( `${this.config.name}: When using AWS_IAM signature, you must also specify the required iamRoleStatements`, ); } @@ -370,7 +372,7 @@ export class DataSource { /^https:\/\/([a-z0-9-]+\.(\w{2}-[a-z]+-\d)\.es\.amazonaws\.com)$/; const result = rx.exec(this.config.config.endpoint); if (!result) { - throw new Error( + throw new this.api.plugin.serverless.classes.Error( `Invalid AWS OpenSearch endpoint: '${this.config.config.endpoint}`, ); } @@ -395,7 +397,7 @@ export class DataSource { ], }; } else { - throw new Error( + throw new this.api.plugin.serverless.classes.Error( `Could not determine the Arn for dataSource '${this.config.name}`, ); } diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index cc74fe1c..f0e47fc6 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -127,7 +127,10 @@ export class Resolver { // Add dependacy to the schema for the full appsync configs if (!isSharedApiConfig(this.api.config)) { - if (!this.api.naming) throw Error('Unable to load the naming module'); + if (!this.api.naming) + throw new this.api.plugin.serverless.classes.Error( + 'Unable to load the naming module', + ); logicalResolver.DependsOn = [this.api.naming.getSchemaLogicalId()]; } diff --git a/src/resources/Schema.ts b/src/resources/Schema.ts index e2759dc2..370f60c5 100644 --- a/src/resources/Schema.ts +++ b/src/resources/Schema.ts @@ -35,10 +35,14 @@ export class Schema { compile(): CfnResources { if (isSharedApiConfig(this.api.config)) { - throw Error('Unable to override shared api schemas'); + throw new this.api.plugin.serverless.classes.Error( + 'Unable to override shared api schemas', + ); } if (!this.api.naming) { - throw Error('Unable to load the naming module'); + throw new this.api.plugin.serverless.classes.Error( + 'Unable to load the naming module', + ); } const logicalId = this.api.naming.getSchemaLogicalId(); diff --git a/src/resources/SyncConfig.ts b/src/resources/SyncConfig.ts index 54ef5526..f830be5e 100644 --- a/src/resources/SyncConfig.ts +++ b/src/resources/SyncConfig.ts @@ -13,10 +13,14 @@ export class SyncConfig { compile() { if (isSharedApiConfig(this.api.config)) { - throw Error('Unable to set the sync config for a Shared AppsyncApi'); + throw new this.api.plugin.serverless.classes.Error( + 'Unable to set the sync config for a Shared AppsyncApi', + ); } if (!this.api.naming) { - throw Error('Unable to Load the naming module'); + throw new this.api.plugin.serverless.classes.Error( + 'Unable to Load the naming module', + ); } if (!this.config.sync) { return undefined; diff --git a/src/resources/Waf.ts b/src/resources/Waf.ts index 5d08ba4d..ca7a3ad7 100644 --- a/src/resources/Waf.ts +++ b/src/resources/Waf.ts @@ -24,10 +24,14 @@ export class Waf { const wafConfig = this.config; if (wafConfig.enabled === false) return {}; if (isSharedApiConfig(this.api.config)) { - throw Error('WAF cannot be specified on existing appsync apis'); + throw new this.api.plugin.serverless.classes.Error( + 'WAF cannot be specified on existing appsync apis', + ); } if (!this.api.naming) { - throw Error('Unable to load the naming module'); + throw new this.api.plugin.serverless.classes.Error( + 'Unable to load the naming module', + ); } const apiLogicalId = this.api.naming.getApiLogicalId(); const wafAssocLogicalId = this.api.naming.getWafAssociationLogicalId(); @@ -135,7 +139,9 @@ export class Waf { buildApiKeysWafRules(): CfnWafRule[] { if (isSharedApiConfig(this.api.config)) { - throw Error('WAF cannot be specified on existing appsync apis'); + throw new this.api.plugin.serverless.classes.Error( + 'WAF cannot be specified on existing appsync apis', + ); } return ( reduce( @@ -148,11 +154,15 @@ export class Waf { buildApiKeyRules(key: ApiKeyConfig) { if (isSharedApiConfig(this.api.config)) { - throw Error('WAF cannot be specified on existing appsync apis'); + throw new this.api.plugin.serverless.classes.Error( + 'WAF cannot be specified on existing appsync apis', + ); } if (!this.api.naming) { // I needed to change the loop to a forof loop to avoid making this check at every loop cycle - throw Error('Unable to load the naming module'); + throw new this.api.plugin.serverless.classes.Error( + 'Unable to load the naming module', + ); } const rules = key.wafRules ?? []; // Build the rule and add a matching rule for the X-Api-Key header From 78abeb6077bf1261f155f563210101cfc69f36e2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 16:40:42 +0100 Subject: [PATCH 21/30] better handeling of disabled functionalities for shared config --- src/index.ts | 33 ++++++++++++++++++--------------- src/resources/Api.ts | 2 +- src/resources/JsResolver.ts | 2 +- src/resources/Resolver.ts | 2 +- src/validation.ts | 2 +- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2a58fee9..b1195ec8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -406,10 +406,10 @@ class ServerlessAppsyncPlugin { } async gatherData() { + // Don't Gather any data for shared api + if(this.config && isSharedApiConfig(this.config)) return; + const apiId = await this.getApiId(); - - // TODO : Check if the api key was provided from the config - //! This function should not be run from a child service stacck if (!apiId) { throw new this.serverless.classes.Error('Unable to get AppSync Api Id'); } @@ -445,11 +445,10 @@ class ServerlessAppsyncPlugin { } async getIntrospection() { + // Never touch the schema for shared api endpoints + if (!this.api?.config || isSharedApiConfig(this.api.config)) return; + const apiId = await this.getApiId(); - - // TODO : Check if the api key was provided from the config - //! This function should not be run from a child service stacck - const { schema } = await this.provider.request< GetIntrospectionSchemaRequest, GetIntrospectionSchemaResponse @@ -720,10 +719,10 @@ class ServerlessAppsyncPlugin { } async assocDomain() { + if (this.api?.config && isSharedApiConfig(this.api.config)) + throw new this.serverless.classes.Error('Inpossible to associate a domain to a shared api'); + const apiId = await this.getApiId(); - - // TODO : Check if the api key was provided from the config - //! This function should not be run from a child service stacck if (typeof apiId !== 'string') { return; } @@ -966,11 +965,9 @@ class ServerlessAppsyncPlugin { } displayEndpoints() { - if (!this.api?.config || isSharedApiConfig(this.api.config)) { - throw new this.serverless.classes.Error( - 'Impossible to display endpoints from a Shared Appsync', - ); - } + // Don't display endpoints for shared api endpoints + if (!this.api?.config || isSharedApiConfig(this.api.config)) return; + const endpoints = this.gatheredData.apis.map( ({ type, uri }) => `${type}: ${uri}`, ); @@ -990,6 +987,9 @@ class ServerlessAppsyncPlugin { } displayApiKeys() { + // Never show api keys shared api endpoints + if (!this.api?.config || isSharedApiConfig(this.api.config)) return; + const { conceal } = this.options; const apiKeys = this.gatheredData.apiKeys.map( ({ description, value }) => `${value} (${description})`, @@ -1024,6 +1024,9 @@ class ServerlessAppsyncPlugin { } validateSchemas() { + // Never validate schema for shared api endpoints + if (!this.api?.config || isSharedApiConfig(this.api.config)) return; + try { this.utils.log.info('Validating AppSync schema'); if (!this.api) { diff --git a/src/resources/Api.ts b/src/resources/Api.ts index d36ac20a..6c2227b3 100644 --- a/src/resources/Api.ts +++ b/src/resources/Api.ts @@ -592,7 +592,7 @@ export class Api { : lambdaArn; } - // Todo: Same syntax for apiId ? + // Todo: [cleanup] Same syntax for apiId ? hasDataSource(name: string) { return name in this.config.dataSources; } diff --git a/src/resources/JsResolver.ts b/src/resources/JsResolver.ts index ffd669f1..58498c24 100644 --- a/src/resources/JsResolver.ts +++ b/src/resources/JsResolver.ts @@ -24,7 +24,7 @@ export class JsResolver { getResolverContent(): string { if (isSharedApiConfig(this.api.config)) { - // Todo : handle js resolvers with config from the parent stack + // Todo : [feature] handle js resolvers with config from the parent stack console.warn('esbuild config is ignored for shared appsync'); return fs.readFileSync(this.config.path, 'utf8'); } diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index f0e47fc6..d56cccb0 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -61,7 +61,7 @@ export class Resolver { } if (isSharedApiConfig(this.api.config)) { - // Todo : handle resolvers caching & sync with config from the parent stack + // Todo : [feature] handle resolvers caching & sync with config from the parent stack console.warn('caching and sync config are ignored for shared appsync'); } else { if (this.config.caching) { diff --git a/src/validation.ts b/src/validation.ts index 1722fec5..ff493062 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -49,7 +49,7 @@ export const sharedAppSyncSchema = { definitions, properties: { ...commonProperties, - apiId: { type: 'string' }, // properties.apiId, // TODO: Handle intrinsic function + apiId: { type: 'string' }, }, required: ['apiId'], additionalProperties: { From aa51d57284c5a17490134e3771871a2f7fc8a4b9 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 18:45:34 +0100 Subject: [PATCH 22/30] Display service output section --- src/index.ts | 22 +++++++++++++++------- src/resources/Resolver.ts | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index b1195ec8..7dc94fbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -968,22 +968,26 @@ class ServerlessAppsyncPlugin { // Don't display endpoints for shared api endpoints if (!this.api?.config || isSharedApiConfig(this.api.config)) return; + const endpoints = this.gatheredData.apis.map( ({ type, uri }) => `${type}: ${uri}`, ); - + if (endpoints.length === 0) return; - + const { name } = this.api.config?.domain || {}; if (name) { endpoints.push(`graphql: https://${name}/graphql`); endpoints.push(`realtime: wss://${name}/graphql/realtime`); } + + + this.utils.writeText('appsync endpoints:') + for (const uri of endpoints.sort()) { + this.utils.writeText(' '+uri) + } + this.utils.writeText('') - this.serverless.addServiceOutputSection( - 'appsync endpoints', - endpoints.sort(), - ); } displayApiKeys() { @@ -1000,7 +1004,11 @@ class ServerlessAppsyncPlugin { } if (!conceal) { - this.serverless.addServiceOutputSection('appsync api keys', apiKeys); + this.utils.writeText('appsync api keys') + for (const key of apiKeys) { + this.utils.writeText(' '+key) + } + this.utils.writeText('') } } diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index d56cccb0..1b5e9e7f 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -62,7 +62,7 @@ export class Resolver { if (isSharedApiConfig(this.api.config)) { // Todo : [feature] handle resolvers caching & sync with config from the parent stack - console.warn('caching and sync config are ignored for shared appsync'); + this.api.plugin.utils.log.warning('caching and sync config are ignored for shared appsync') } else { if (this.config.caching) { if (this.config.caching === true) { From 35497cb2981e0d87467f3e72a7ae08eeb82d1828 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 19:09:42 +0100 Subject: [PATCH 23/30] cleanup --- src/types/index.ts | 2 +- src/types/plugin.ts | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 2c30f17e..2b300c20 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,8 +12,8 @@ import type { DsEventBridgeConfig, DsHttpConfig, DsLambdaConfig, - DsOpenSearchConfig, DsNone, + DsOpenSearchConfig, DsRelationalDbConfig, SyncConfig, EnvironmentVariables, diff --git a/src/types/plugin.ts b/src/types/plugin.ts index cb9c925a..6709513e 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -1,21 +1,21 @@ //* Internal typing : Used in the plugin exclusively import type { BuildOptions } from 'esbuild'; import type { - ApiKeyConfig, Auth, - Substitutions, - CachingConfig, DomainConfig, + ApiKeyConfig, LoggingConfig, + CachingConfig, WafConfig, - DsDynamoDBConfig, - DsEventBridgeConfig, + SyncConfig, DsHttpConfig, - DsLambdaConfig, + DsDynamoDBConfig, + DsRelationalDbConfig, DsOpenSearchConfig, + DsLambdaConfig, + DsEventBridgeConfig, DsNone, - DsRelationalDbConfig, - SyncConfig, + Substitutions, EnvironmentVariables, } from './common.js'; export * from './common.js'; @@ -28,7 +28,7 @@ export type BaseAppSyncConfig = { }; export type FullAppSyncConfig = BaseAppSyncConfig & { name: string; - schema?: string[]; + schema: string[]; authentication: Auth; additionalAuthentications: Auth[]; domain?: DomainConfig; @@ -86,7 +86,7 @@ export type PipelineResolverConfig = BaseResolverConfig & { }; export type DataSourceConfig = { - name: string; // Not avalible in external types (index.ts) + name: string; description?: string; } & ( | DsHttpConfig @@ -99,7 +99,7 @@ export type DataSourceConfig = { ); export type PipelineFunctionConfig = { - name: string; // Not avalible in external types (index.ts) + name: string; dataSource: string; description?: string; code?: string; From cbdc2d89f521f8969a4bce6103d4f96923df9e55 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 19:11:49 +0100 Subject: [PATCH 24/30] cleanup --- tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index b22c3b65..3f797e49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,7 @@ "skipLibCheck": true, "outDir": "lib", "noImplicitAny": false, - "declaration": true, - "forceConsistentCasingInFileNames": true + "declaration": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "src/__tests__"] From 6396da38a0524b4350d83c3b33f9be54e9ca24d5 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Nov 2024 19:12:11 +0100 Subject: [PATCH 25/30] cleanup --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 3f797e49..8cf63f7b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,4 +15,4 @@ }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "src/__tests__"] -} \ No newline at end of file +} From 6cbcf2eacac08577063b3d0073bfa3998f04ea90 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 20 Nov 2024 15:54:04 +0100 Subject: [PATCH 26/30] added loggingfor schema generation --- src/resources/Schema.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/resources/Schema.ts b/src/resources/Schema.ts index 370f60c5..853dbe40 100644 --- a/src/resources/Schema.ts +++ b/src/resources/Schema.ts @@ -71,10 +71,17 @@ export class Schema { const cwd = this.api.plugin.serverless.config.servicePath; const schemaFiles = flatten(globby.sync(this.schemas, { cwd })); + this.api.plugin.utils.log.info('loading schema from :') + this.api.plugin.utils.log.info(schemaFiles.join('\n')) const schemas = schemaFiles.map((file) => { return fs.readFileSync(path.join(cwd, file), 'utf8'); }); + if (schemas.join('\n').length < 1) { + throw new this.api.plugin.serverless.classes.Error( + `AppSync schema should not be empty - cwd: ${cwd}` + ) + } this.valdiateSchema(AWS_TYPES + '\n' + schemas.join('\n')); // Return single files as-is. From 4109ae3d59219f0d8c7b08981305b48d96718151 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 27 Nov 2024 21:40:36 +0100 Subject: [PATCH 27/30] TODO --- src/resources/Resolver.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 1b5e9e7f..8a21e74d 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -62,7 +62,9 @@ export class Resolver { if (isSharedApiConfig(this.api.config)) { // Todo : [feature] handle resolvers caching & sync with config from the parent stack - this.api.plugin.utils.log.warning('caching and sync config are ignored for shared appsync') + this.api.plugin.utils.log.warning( + 'caching and sync config are ignored for shared appsync', + ); } else { if (this.config.caching) { if (this.config.caching === true) { @@ -87,6 +89,7 @@ export class Resolver { if (this.config.kind === 'UNIT') { const { dataSource } = this.config; + // TODO: [feature] support existing datasource (by providing an id ?) if (!this.api.hasDataSource(dataSource)) { throw new this.api.plugin.serverless.classes.Error( `Resolver '${this.config.type}.${this.config.field}' references unknown DataSource '${dataSource}'`, From 976992dc72afb3e8e85e01d84034303e14a089f8 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 28 Nov 2024 19:38:25 +0100 Subject: [PATCH 28/30] feat/handle datasources from other stacks --- src/resources/Resolver.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 8a21e74d..3dc9a841 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -89,18 +89,31 @@ export class Resolver { if (this.config.kind === 'UNIT') { const { dataSource } = this.config; - // TODO: [feature] support existing datasource (by providing an id ?) - if (!this.api.hasDataSource(dataSource)) { + + if ( + !isSharedApiConfig(this.api.config) && + !this.api.hasDataSource(dataSource) + ) { throw new this.api.plugin.serverless.classes.Error( `Resolver '${this.config.type}.${this.config.field}' references unknown DataSource '${dataSource}'`, ); } - const logicalIdDataSource = Naming.getDataSourceLogicalId(dataSource); + // Handle datasources defined in existing appsync config + // if the datasource is not found in the current config, use the datasource name instead of the logical id. + const dataSourceLogicalId = Naming.getDataSourceLogicalId(dataSource); + const dataSourceName = + isSharedApiConfig(this.api.config) && + !this.api.hasDataSource(dataSource) + ? dataSource + : ({ + 'Fn::GetAtt': [dataSourceLogicalId, 'Name'], + } satisfies IntrinsicFunction); + Properties = { ...Properties, Kind: 'UNIT', - DataSourceName: { 'Fn::GetAtt': [logicalIdDataSource, 'Name'] }, + DataSourceName: dataSourceName, MaxBatchSize: this.config.maxBatchSize, }; } else { From 14a46020168e33d6cc586338d168edaa10256442 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 29 Nov 2024 14:39:50 +0100 Subject: [PATCH 29/30] Handle Substitutions --- src/getAppSyncConfig.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/getAppSyncConfig.ts b/src/getAppSyncConfig.ts index 44b7a46d..77ef73c4 100644 --- a/src/getAppSyncConfig.ts +++ b/src/getAppSyncConfig.ts @@ -14,6 +14,7 @@ import type { BaseAppSyncConfig, SharedAppSyncConfig, FullAppSyncConfig, + Substitutions, } from './types/plugin.js'; import { forEach, merge } from 'lodash-es'; @@ -95,6 +96,7 @@ function getBaseAppsyncConfig(config: AppSyncConfig): BaseAppSyncConfig { const dataSources: Record = {}; const resolvers: Record = {}; const pipelineFunctions: Record = {}; + const substitutions: Substitutions = {}; forEach(flattenMaps(config.dataSources), (ds, name) => { dataSources[name] = { @@ -177,9 +179,14 @@ function getBaseAppsyncConfig(config: AppSyncConfig): BaseAppSyncConfig { }; }); + if (config.substitutions) { + Object.assign(substitutions, config.substitutions); + } + return { dataSources, resolvers, pipelineFunctions, + substitutions, }; } From 5b70750e2cb9b10b765ec5f695358d2fdf2fbecc Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 29 Nov 2024 14:57:50 +0100 Subject: [PATCH 30/30] Handle pipelines functions on child stack --- src/resources/PipelineFunction.ts | 20 ++++++++++++++------ src/resources/Resolver.ts | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/resources/PipelineFunction.ts b/src/resources/PipelineFunction.ts index d5fb4d1e..422451b3 100644 --- a/src/resources/PipelineFunction.ts +++ b/src/resources/PipelineFunction.ts @@ -3,7 +3,7 @@ import { CfnResources, IntrinsicFunction, } from '../types/cloudFormation.js'; -import { PipelineFunctionConfig } from '../types/plugin.js'; +import { isSharedApiConfig, PipelineFunctionConfig } from '../types/plugin.js'; import { Api } from './Api.js'; import path from 'path'; import { MappingTemplate } from './MappingTemplate.js'; @@ -16,21 +16,29 @@ export class PipelineFunction { compile(): CfnResources { const { dataSource, code } = this.config; - if (!this.api.hasDataSource(dataSource)) { + if ( + !isSharedApiConfig(this.api.config) && + !this.api.hasDataSource(dataSource) + ) { throw new this.api.plugin.serverless.classes.Error( `Pipeline Function '${this.config.name}' references unknown DataSource '${dataSource}'`, ); } const logicalId = Naming.getPipelineFunctionLogicalId(this.config.name); - const logicalIdDataSource = Naming.getDataSourceLogicalId( - this.config.dataSource, - ); + const logicalIdDataSource = Naming.getDataSourceLogicalId(dataSource); + + const dataSourceName = + isSharedApiConfig(this.api.config) && !this.api.hasDataSource(dataSource) + ? dataSource + : ({ + 'Fn::GetAtt': [logicalIdDataSource, 'Name'], + } satisfies IntrinsicFunction); const Properties: CfnFunctionResolver['Properties'] = { ApiId: this.api.getApiId(), Name: this.config.name, - DataSourceName: { 'Fn::GetAtt': [logicalIdDataSource, 'Name'] }, + DataSourceName: dataSourceName, Description: this.config.description, FunctionVersion: '2018-05-29', MaxBatchSize: this.config.maxBatchSize, diff --git a/src/resources/Resolver.ts b/src/resources/Resolver.ts index 3dc9a841..b8b99db3 100644 --- a/src/resources/Resolver.ts +++ b/src/resources/Resolver.ts @@ -101,13 +101,13 @@ export class Resolver { // Handle datasources defined in existing appsync config // if the datasource is not found in the current config, use the datasource name instead of the logical id. - const dataSourceLogicalId = Naming.getDataSourceLogicalId(dataSource); + const logicalIdDataSource = Naming.getDataSourceLogicalId(dataSource); const dataSourceName = isSharedApiConfig(this.api.config) && !this.api.hasDataSource(dataSource) ? dataSource : ({ - 'Fn::GetAtt': [dataSourceLogicalId, 'Name'], + 'Fn::GetAtt': [logicalIdDataSource, 'Name'], } satisfies IntrinsicFunction); Properties = {