Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This package provides an implementation for the `@aws-appsync/utils` package tha

## Changelog:

- v0.1.9: add `runtime.earlyReturn()`, importable from `@aws-appsync/utils`; it validates the `skipTo` return option and hands the value and target to the host, which decides which handlers still run
- v0.1.8: `util.authType()` now reports `Lambda Authorization` for the identity of a Lambda-authorized Event API request, which carries what the authorizer returned as `handlerContext` rather than the `resolverContext` a GraphQL API uses
- v0.1.7: complete the `@aws-appsync/utils/dynamodb` module with `query`, `sync`, the batch and transact helpers, the set helpers, `operations.remove` and `operations.updateListItem`, and the arguments the existing helpers dropped (`filter`, `projection`, `condition`, `consistentRead`, `_version` and the rest); add `util.transform.toSubscriptionFilter`; `util.dynamodb.fromS3ObjectJson` now parses instead of throwing
- v0.1.6: add `util.authType()`, derived from the identity of the request; a host installs the request through the new `setResolverContext` export. `util.transform.toDynamoDBFilterExpression` and `toDynamoDBConditionExpression` now cover every operator, including `between`, `in`, `size` and `attributeType`, and nested `and`/`or`/`not`; `util.dynamodb.toDynamoDB` accepts `null`
Expand Down
25 changes: 25 additions & 0 deletions __tests__/__snapshots__/runtime.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`runtime.earlyReturn accepts skipTo END 1`] = `"end"`;

exports[`runtime.earlyReturn accepts skipTo NEXT 1`] = `"next"`;

exports[`runtime.earlyReturn rejects an unknown skipTo 1`] = `
{
"message": "SkipTo only supports input \`END\` or \`NEXT\` ",
}
`;

exports[`runtime.earlyReturn rejects options without skipTo 1`] = `
{
"message": "ReturnOptions parameter must contain a 'skipTo' key ",
}
`;

exports[`runtime.earlyReturn returns null without a value 1`] = `null`;

exports[`runtime.earlyReturn returns the value 1`] = `
{
"early": true,
}
`;
14 changes: 9 additions & 5 deletions __tests__/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { fileURLToPath, pathToFileURL } from "url";
import { dirname, resolve } from "path";
import { AppSyncClient, EvaluateCodeCommand } from "@aws-sdk/client-appsync";
import { util, setResolverContext } from "..";
import { util, runtime, setResolverContext } from "..";
import * as ddb from "../dynamodb";
import * as rds from "../rds";

Expand Down Expand Up @@ -70,13 +70,13 @@ export const checkResolverValid = async (code, context, functionName) => {
let result;
if (process.env.TEST_TARGET === "AWS_CLOUD") {
const fullCode = `
import { util } from '@aws-appsync/utils';
import { util, runtime } from '@aws-appsync/utils';
import * as rds from '@aws-appsync/utils/rds';
` + code;
result = await runResolverFunctionOnAWS(fullCode, context, functionName);
} else {
const fullCode = `
import { util } from '${pathToFileURL(resolve(projectRoot, "index.js"))}';
import { util, runtime } from '${pathToFileURL(resolve(projectRoot, "index.js"))}';
import * as rds from '${pathToFileURL(resolve(projectRoot, "rds/index.js"))}';
` + code;
const encodedJs = encodeURIComponent(fullCode);
Expand All @@ -92,10 +92,14 @@ export const checkResolverValid = async (code, context, functionName) => {
try {
result = fn(context);
} catch (e) {
if (e.name !== "AppSyncUserError") {
if (e.name === "AppSyncEarlyReturn") {
// what the host does with the value it returned early: on AWS it is the evaluation result
result = e.value ?? null;
} else if (e.name === "AppSyncUserError") {
result = {"message": e.message}
} else {
throw e;
}
result = {"message": e.message}
} finally {
setResolverContext(null);
}
Expand Down
39 changes: 39 additions & 0 deletions __tests__/runtime.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, test } from "@jest/globals";

import { checkResolverValid } from "./helpers";

const resolver = (body) => `
export function request(ctx) {
${body}
}

export function response(ctx) {
return ctx.result;
}
`;

describe("runtime.earlyReturn", () => {
test("returns the value", async () => {
await checkResolverValid(resolver(`runtime.earlyReturn({ early: true }); return { late: true };`), {}, "request");
});

test("returns null without a value", async () => {
await checkResolverValid(resolver(`runtime.earlyReturn();`), {}, "request");
});

test("accepts skipTo END", async () => {
await checkResolverValid(resolver(`runtime.earlyReturn("end", { skipTo: "END" });`), {}, "request");
});

test("accepts skipTo NEXT", async () => {
await checkResolverValid(resolver(`runtime.earlyReturn("next", { skipTo: "NEXT" });`), {}, "request");
});

test("rejects an unknown skipTo", async () => {
await checkResolverValid(resolver(`runtime.earlyReturn("x", { skipTo: "BOGUS" });`), {}, "request");
});

test("rejects options without skipTo", async () => {
await checkResolverValid(resolver(`runtime.earlyReturn("x", {});`), {}, "request");
});
});
30 changes: 29 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { v4 as uuidv4 } from 'uuid';
import { toJsonObject } from './rds/index.js'
import { AppSyncUserError } from './errors.js'
import { AppSyncUserError, codeError } from './errors.js'

export const dynamodbUtils = {
toDynamoDB: function(value) {
Expand Down Expand Up @@ -523,3 +523,31 @@ function toSubscriptionFilter(filter, ignoredFields, rules) {
// a group that collected no filter at all is dropped, so an empty filter yields an empty list
return { filterGroup: groups.filter((filters) => filters.length).map((filters) => ({ filters })) };
}

// Unwinds the handler that called `runtime.earlyReturn`. The host recognises it by name, takes
// `value` as the handler's result, and uses `skipTo` to decide which handlers still run.
class AppSyncEarlyReturn extends Error {
constructor(value, skipTo) {
super("runtime.earlyReturn");
this.name = "AppSyncEarlyReturn";
this.value = value;
this.skipTo = skipTo;
}
}

const SKIP_TO_TARGETS = ["END", "NEXT"];

export const runtime = {
earlyReturn: function(obj, returnOptions) {
if (returnOptions === undefined || returnOptions === null) {
throw new AppSyncEarlyReturn(obj, "NEXT");
}
if (!Object.hasOwn(returnOptions, "skipTo")) {
throw codeError("ReturnOptions parameter must contain a 'skipTo' key ");
}
if (!SKIP_TO_TARGETS.includes(returnOptions.skipTo)) {
throw codeError("SkipTo only supports input `END` or `NEXT` ");
}
throw new AppSyncEarlyReturn(obj, returnOptions.skipTo);
},
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@localstack/appsync-utils",
"version": "0.1.8",
"version": "0.1.9",
"description": "Implementation of the AppSync utils helpers",
"type": "module",
"main": "index.js",
Expand Down
Loading