From 51eb60806ea48eda2f9f2ae0d2421e67a936ab72 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 26 Aug 2024 10:52:58 +0200 Subject: [PATCH 1/3] Convert modern GraphQL description into comments that are supported by AppSync --- .../fixtures/schemas/multiple/post.graphql | 9 ++- src/__tests__/schema.test.ts | 36 +++++++---- src/resources/Schema.ts | 64 +++++++++++++++---- 3 files changed, 84 insertions(+), 25 deletions(-) diff --git a/src/__tests__/fixtures/schemas/multiple/post.graphql b/src/__tests__/fixtures/schemas/multiple/post.graphql index 7af0864d..1fbb96fa 100644 --- a/src/__tests__/fixtures/schemas/multiple/post.graphql +++ b/src/__tests__/fixtures/schemas/multiple/post.graphql @@ -1,8 +1,15 @@ extend type Query { - getPost(id: ID!): Post! + getPost( + "This is an inline description" + id: ID! + ): Post! } extend type Mutation { + """ + This is a description + that includes multiple lines + """ createPost(post: PostInput!): Post! } diff --git a/src/__tests__/schema.test.ts b/src/__tests__/schema.test.ts index deae80fe..8adc193d 100644 --- a/src/__tests__/schema.test.ts +++ b/src/__tests__/schema.test.ts @@ -31,9 +31,7 @@ describe('schema', () => { createUser(post: UserInput!): User! } - \\"\\"\\" - A User - \\"\\"\\" + #A User type User { id: ID! name: String! @@ -43,6 +41,7 @@ describe('schema', () => { input UserInput { name: String! } + ", }, "Type": "AWS::AppSync::GraphQLSchema", @@ -60,6 +59,8 @@ describe('schema', () => { ]); expect(schema.generateSchema()).toMatchInlineSnapshot(` "type Mutation { + #This is a description + #that includes multiple lines createPost(post: PostInput!): Post! createUser(post: UserInput!): User! } @@ -71,13 +72,16 @@ describe('schema', () => { updatedAt: AWSDateTime! } - \\"\\"\\"This is a description\\"\\"\\" + #This is a description input PostInput { title: String! } type Query { - getPost(id: ID!): Post! + getPost( + #This is an inline description + id: ID! + ): Post! getUser: User! } @@ -91,7 +95,8 @@ describe('schema', () => { input UserInput { name: String! - }" + } + " `); }); @@ -102,6 +107,8 @@ describe('schema', () => { ]); expect(schema.generateSchema()).toMatchInlineSnapshot(` "type Mutation { + #This is a description + #that includes multiple lines createPost(post: PostInput!): Post! createUser(post: UserInput!): User! } @@ -113,13 +120,16 @@ describe('schema', () => { updatedAt: AWSDateTime! } - \\"\\"\\"This is a description\\"\\"\\" + #This is a description input PostInput { title: String! } type Query { - getPost(id: ID!): Post! + getPost( + #This is an inline description + id: ID! + ): Post! getUser: User! } @@ -133,7 +143,8 @@ describe('schema', () => { input UserInput { name: String! - }" + } + " `); }); @@ -153,7 +164,7 @@ describe('schema', () => { `); }); - it('should return single files schemas as-is', () => { + it('should return single files schemas with converted descriptions', () => { const api = new Api(given.appSyncConfig(), plugin); const schema = new Schema(api, [ 'src/__tests__/fixtures/schemas/single/schema.graphql', @@ -167,9 +178,7 @@ describe('schema', () => { createUser(post: UserInput!): User! } - \\"\\"\\" - A User - \\"\\"\\" + #A User type User { id: ID! name: String! @@ -179,6 +188,7 @@ describe('schema', () => { input UserInput { name: String! } + " `); }); diff --git a/src/resources/Schema.ts b/src/resources/Schema.ts index 1c958d13..9dd9b2b2 100644 --- a/src/resources/Schema.ts +++ b/src/resources/Schema.ts @@ -5,7 +5,6 @@ import { CfnResources } from '../types/cloudFormation'; import { Api } from './Api'; import { flatten } from 'lodash'; import { parse, print } from 'graphql'; -import ServerlessError from 'serverless/lib/serverless-error'; import { validateSDL } from 'graphql/validation/validate'; import { mergeTypeDefs } from '@graphql-tools/merge'; @@ -47,7 +46,7 @@ export class Schema { }; } - valdiateSchema(schema: string) { + validateSchema(schema: string) { const errors = validateSDL(parse(schema)); if (errors.length > 0) { throw new this.api.plugin.serverless.classes.Error( @@ -57,6 +56,47 @@ export class Schema { } } + // AppSync does not support descriptions from June 2018 spec + // https://spec.graphql.org/June2018/#sec-Descriptions + // so they need to be converted to comments, the space after the # will also be included + // by AppSync in the generated description so we remove it + convertDescriptions(schema: string): string { + const lines = schema.split('\n'); + const singleLineComment = /^(? *)"(?[^"]+?)"$/; + const singleLineMultilineComment = /^(? *)"""(?.+?)"""$/; + const multilineCommentDelimiter = /^(? *)"""$/; + + let inComment = false; + let result = ''; + + for (const line of lines) { + switch (true) { + case singleLineComment.test(line): + result += `${line.match(singleLineComment)?.groups?.indent}#${ + line.match(singleLineComment)?.groups?.comment + }\n`; + break; + case singleLineMultilineComment.test(line): + result += `${ + line.match(singleLineMultilineComment)?.groups?.indent + }#${line.match(singleLineMultilineComment)?.groups?.comment}\n`; + break; + case multilineCommentDelimiter.test(line): + inComment = !inComment; + break; + case inComment: + result += `${ + line.match(/^(? *)/)?.groups?.indent + }#${line.trimStart()}\n`; + break; + default: + result += line + '\n'; + } + } + + return result; + } + generateSchema() { const schemaFiles = flatten(globby.sync(this.schemas)); @@ -67,23 +107,25 @@ export class Schema { ); }); - this.valdiateSchema(AWS_TYPES + '\n' + schemas.join('\n')); + this.validateSchema(AWS_TYPES + '\n' + schemas.join('\n')); // Return single files as-is. if (schemas.length === 1) { - return schemas[0]; + return this.convertDescriptions(schemas[0]); } // AppSync does not support Object extensions // https://spec.graphql.org/October2021/#sec-Object-Extensions // Merge the schemas - return print( - mergeTypeDefs(schemas, { - forceSchemaDefinition: false, - useSchemaDefinition: false, - sort: true, - throwOnConflict: true, - }), + return this.convertDescriptions( + print( + mergeTypeDefs(schemas, { + forceSchemaDefinition: false, + useSchemaDefinition: false, + sort: true, + throwOnConflict: true, + }), + ), ); } } From bead371f482e2660ea752f32e2ce172549a7e922 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 29 Sep 2026 12:58:49 +0200 Subject: [PATCH 2/3] fix: use osls 4 SDK v3 config for AWS credentials osls 4 removed provider.getCredentials(), so the plugin fell back to the default credential chain and ignored provider.profile and --aws-profile. Use provider.getAwsSdkV3Config() credentials when it exists. --- src/__tests__/credentials.test.ts | 59 +++++++++++++++++++++++++++++++ src/index.ts | 11 ++++++ src/types/serverless.d.ts | 8 +++++ 3 files changed, 78 insertions(+) create mode 100644 src/__tests__/credentials.test.ts diff --git a/src/__tests__/credentials.test.ts b/src/__tests__/credentials.test.ts new file mode 100644 index 00000000..c294b8e6 --- /dev/null +++ b/src/__tests__/credentials.test.ts @@ -0,0 +1,59 @@ +import * as given from './given'; + +jest.mock('@aws-sdk/credential-providers', () => ({ + fromNodeProviderChain: () => async () => ({ + accessKeyId: 'DEFAULT_CHAIN', + secretAccessKey: 'default-chain', + }), +})); + +type CredentialProvider = () => Promise>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const resolveCredentials = (plugin: any): Promise> => + (plugin.clientFactory.credentials as CredentialProvider)(); + +describe('AWS credentials', () => { + // osls 4 removed provider.getCredentials(); calling it throws + // AWS_SDK_V2_SURFACE_REMOVED. Falling back to the default chain would ignore + // provider.profile and --aws-profile, so the osls-resolved config must win. + it('uses the credentials osls 4 resolves via getAwsSdkV3Config()', async () => { + const plugin = given.plugin(); + const identity = { accessKeyId: 'OSLS', secretAccessKey: 'osls' }; + Object.assign(plugin['provider'], { + getAwsSdkV3Config: jest.fn().mockResolvedValue({ + region: 'eu-west-1', + credentials: async () => identity, + }), + getCredentials: () => { + throw new Error('AWS_SDK_V2_SURFACE_REMOVED'); + }, + }); + + await expect(resolveCredentials(plugin)).resolves.toEqual(identity); + }); + + it('accepts static credentials from getAwsSdkV3Config()', async () => { + const plugin = given.plugin(); + const identity = { accessKeyId: 'STATIC', secretAccessKey: 'static' }; + Object.assign(plugin['provider'], { + getAwsSdkV3Config: jest.fn().mockResolvedValue({ credentials: identity }), + }); + + await expect(resolveCredentials(plugin)).resolves.toEqual(identity); + }); + + it('uses getCredentials() on Serverless 3 / osls 3', async () => { + const plugin = given.plugin(); + Object.assign(plugin['provider'], { + getCredentials: () => ({ + credentials: { accessKeyId: 'SLS3', secretAccessKey: 'sls3' }, + }), + }); + + await expect(resolveCredentials(plugin)).resolves.toMatchObject({ + accessKeyId: 'SLS3', + secretAccessKey: 'sls3', + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index e32fbe6d..ed244f89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,6 +70,17 @@ const CONSOLE_BASE_URL = 'https://console.aws.amazon.com'; */ const resolveCredentials = (provider: Provider): AwsCredentials => { return async () => { + // osls 4 removed `getCredentials()` (it throws AWS_SDK_V2_SURFACE_REMOVED) + // and exposes its resolved SDK v3 client config instead. + if (typeof provider.getAwsSdkV3Config === 'function') { + const { credentials: v3Credentials } = await provider.getAwsSdkV3Config(); + if (!v3Credentials) { + return fromNodeProviderChain()(); + } + return typeof v3Credentials === 'function' + ? v3Credentials() + : v3Credentials; + } let credentials; try { ({ credentials } = provider.getCredentials()); diff --git a/src/types/serverless.d.ts b/src/types/serverless.d.ts index f6ffd362..a9e271ad 100644 --- a/src/types/serverless.d.ts +++ b/src/types/serverless.d.ts @@ -125,6 +125,14 @@ declare module 'serverless/lib/plugins/aws/provider.js' { region?: string; signatureVersion?: string; }; + // osls 4 only: client config (region, credentials, retries, proxy) for + // plugin-built SDK v3 clients. Replaces request() and getCredentials(). + getAwsSdkV3Config?: () => Promise<{ + region?: string; + credentials?: + | import('@aws-sdk/types').AwsCredentialIdentity + | import('@aws-sdk/types').AwsCredentialIdentityProvider; + }>; } export default Provider; From b3de2fbcc0aa434f8ebe527819cd615839494fe7 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 29 Sep 2026 12:59:26 +0200 Subject: [PATCH 3/3] chore: publish as @monei-js/serverless-appsync-plugin - Rename the package, point repository metadata to the MONEI fork - Replace the tag-triggered NPM_TOKEN release with a GitHub release publish workflow (npm trusted publishing) --- .github/workflows/publish.yml | 22 ++++++++++++ .github/workflows/release.yml | 67 ----------------------------------- README.md | 4 +++ package.json | 9 +++-- 4 files changed, 32 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..0a0edf0c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,22 @@ +name: Publish + +on: + release: + types: [published] + +permissions: + id-token: write + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - run: npm ci + - run: npm test + - run: npm run lint + - run: npm publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index b0ae9b7e..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*.*.*' - -jobs: - release: - name: Release - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: 22 - registry-url: 'https://registry.npmjs.org' - - name: Install dependencies - run: npm ci - - name: Run tests - run: npm run test - - name: Lint - run: npm run lint - - name: Build - run: npm run build - - name: Set version from tag - run: | - VERSION="${GITHUB_REF_NAME#v}" - echo "Setting package.json version to $VERSION" - npm version "$VERSION" --no-git-tag-version --allow-same-version - - name: Publish to npm - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Generate changelog - id: changelog - run: | - PREV_TAG=$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || echo "") - if [ -z "$PREV_TAG" ]; then - COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges) - else - COMMITS=$(git log "$PREV_TAG..${GITHUB_REF_NAME}" --pretty=format:"- %s (%h)" --no-merges) - fi - { - echo "changelog<<__END__" - echo "$COMMITS" - echo "__END__" - } >> "$GITHUB_OUTPUT" - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - name: ${{ github.ref_name }} - body: | - ## What's Changed - - ${{ steps.changelog.outputs.changelog }} - - **Full Changelog**: https://github.com/sid88in/serverless-appsync-plugin/compare/${{ github.ref_name }} - draft: false - prerelease: false diff --git a/README.md b/README.md index 605479b6..9be25348 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +> **MONEI fork**, published as `@monei-js/serverless-appsync-plugin`. On top of upstream it converts GraphQL descriptions to comments (AppSync rejects descriptions) and uses the osls v4 AWS credentials. +> +> Releasing: bump `version` in `package.json`, merge to `master`, then publish a GitHub release for that version. The `Publish` workflow publishes to npm with trusted publishing. +

npm downloads (weekly) npm downloads (year) diff --git a/package.json b/package.json index d56d97ad..8d8e1bdb 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "serverless-appsync-plugin", - "version": "0.0.0-development", + "name": "@monei-js/serverless-appsync-plugin", + "version": "2.14.0", "description": "AWS AppSync support for the Serverless Framework", "main": "lib/index.js", "types": "lib/types/index.d.ts", - "repository": "https://github.com/sid88in/serverless-appsync-plugin", + "repository": "https://github.com/MONEI/serverless-appsync-plugin", "license": "MIT", "files": [ "/lib" @@ -95,5 +95,8 @@ "*.{json,md,yml,yaml}": [ "prettier --write" ] + }, + "publishConfig": { + "access": "public" } }