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.
+
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"
}
}
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/__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 98a6285e..1cb3f0b4 100644
--- a/src/__tests__/schema.test.ts
+++ b/src/__tests__/schema.test.ts
@@ -32,9 +32,7 @@ describe('schema', () => {
createUser(post: UserInput!): User!
}
- """
- A User
- """
+ #A User
type User {
id: ID!
name: String!
@@ -44,6 +42,7 @@ describe('schema', () => {
input UserInput {
name: String!
}
+
",
},
"Type": "AWS::AppSync::GraphQLSchema",
@@ -61,6 +60,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!
}
@@ -72,13 +73,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!
}
@@ -92,7 +96,8 @@ describe('schema', () => {
input UserInput {
name: String!
- }"
+ }
+ "
`);
});
@@ -103,6 +108,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!
}
@@ -114,13 +121,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!
}
@@ -134,7 +144,8 @@ describe('schema', () => {
input UserInput {
name: String!
- }"
+ }
+ "
`);
});
@@ -145,6 +156,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!
}
@@ -156,13 +169,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!
}
@@ -176,7 +192,8 @@ describe('schema', () => {
input UserInput {
name: String!
- }"
+ }
+ "
`);
});
@@ -253,7 +270,7 @@ describe('schema', () => {
expect(schema.generateSchema()).toContain('type Query');
});
- 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',
@@ -267,9 +284,7 @@ describe('schema', () => {
createUser(post: UserInput!): User!
}
- """
- A User
- """
+ #A User
type User {
id: ID!
name: String!
@@ -279,6 +294,7 @@ describe('schema', () => {
input UserInput {
name: String!
}
+
"
`);
});
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/resources/Schema.ts b/src/resources/Schema.ts
index 551e9343..b1d9cbd6 100644
--- a/src/resources/Schema.ts
+++ b/src/resources/Schema.ts
@@ -70,7 +70,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(
@@ -80,6 +80,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(
@@ -98,23 +139,25 @@ export class Schema {
return this.stripReservedDefinitions(fs.readFileSync(file, 'utf8'));
});
- this.valdiateSchema(AWS_TYPES + '\n' + schemas.join('\n'));
+ this.validateSchema(AWS_TYPES + '\n' + schemas.join('\n'));
// Return single (already stripped) 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,
+ }),
+ ),
);
}
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;