TypeScript plugin for service-to-service (aka. "functionless") cloud integrations.

Overview

Functionless λ<

npm version

Functionless is a TypeScript plugin that transforms TypeScript code into Service-to-Service (aka. "functionless") integrations, such as AWS AppSync Resolvers and Velocity Templates, or Amazon States Language for AWS Step Functions.

For example, the below function creates an Appsync Resolver Pipeline with two stages:

  1. Put an item into the postTable DynamoDB Table
  2. Trigger a long-running Step Function workflow to validate the contents
const postTable = new Table<Post, "postId">(new aws_dynamodb.Table(this, "PostTable", { .. }));

// Query.addPost AppSync Resolver
const addPost = new AppsyncResolver<{ title: string, text: string }, Post>(($context) => {
  const post = postDatabase.get({
    key: $util.toDynamoDB($util.autoUuid()),
    title: $util.toDynamoDB($context.arguments.title),
    text: $util.toDynamoDB($context.arguments.text),
  });

  // start execution of a long-running workflow to validate the Post
  validatePostWorkflow(post);

  return post;
});

// a Lambda Function which can validate the contents of a Post
const validatePost = new Function<Post, >(new aws_lambda.Function(this, "Validate", { .. }))

// Step Function workflow that validates the contents of a Post and deletes it if bad
const validatePostWorkflow = new StepFunction(this, "ValidatePostWorkflow", (post: Post) => {
  const validationResult = validatePost(post);
  if (validationResult.status === "Not Cool") {
    $AWS.DynamoDB.DeleteItem({
      TableName: postTable,
      Key: {
        postId: {
          S: post.postId
        }
      }
    });
  }
});

Functionless parses the TypeScript code and converts it to Amazon States Language, Apache Velocity Templates and a CloudFormation configuration, saving you from writing all of that boilerplate.

Why you should use Service-to-Service Integrations

Paul Swail has a piece on this topic which is worth reading: https://serverlessfirst.com/functionless-integration-trade-offs/.

In short: these integrations have many advantages over using AWS Lambda Functions, including:

  1. lower latency - there is no cold start, so a service-to-service integration will feel "snappy" when compared to a Lambda Function.
  2. lower cost - there's no intermediate Lambda Invocation when AppSync calls DynamoDB directly.
  3. higher scalability - the handlers are not subject to Lambda's concurrent invocation limits and are running on dedicated Amazon servers.
  4. no operational maintenance - such as upgrading dependencies, patching security vulnerabilities, etc. - theoretically, once the configuration is confirmed to be correct, it then becomes entirely AWS's responsibility to ensure the code is running optimally.

The downsides of these integrations are their dependence on Domain Specific Languages (DSL) such as Apache Velocity Templates or Amazon States Language JSON. These DSLs are difficult to work with since they lack the type-safety and expressiveness of TypeScript. Functionless aims to solve this problem by converting beautiful, type-safe TypeScript code directly into these configurations.

Setup

First, install the functionless and ts-patch NPM packages.

npm install --save-dev functionless ts-patch

Then, add ts-patch install -s to your prepare script (see ts-patch for mode details.)

{
  "scripts": {
    "prepare": "ts-patch install -s"
  }
}

Make sure to run npm install to bootstrap ts-patch (via the prepare script).

npm install

Finally, configure the functionless/lib/compile TypeScript transformer plugin in your tsconfig.json:

{
  "compilerOptions": {
    "plugins": [
      {
        "transform": "functionless/lib/compile"
      }
    ]
  }
}

Files can be ignored by the transformer by using glob patterns in the tsconfig.json:

{
  "compilerOptions": {
    "plugins": [
      {
        "transform": "functionless/lib/compile",
        "exclude": ["./src/**/protected/*"]
      }
    ]
  }
}

Usage

functionless makes configuring services like AWS Appsync as easy as writing TypeScript functions.

App Sync

There are three aspects your need to learn before using Functionless in your CDK application:

  1. Appsync Integration interfaces for Function and Table.
  2. AppsyncResolver construct for defining Appsync Resolver with TypeScript syntax.
  3. Add Resolvers to an @aws-cdk/aws-appsync-alpha.GraphQLApi.

Appsync Integration interfaces for Function and Table

You must wrap your CDK L2 Constructs in the corresponding wrapper class provided by functionless. Currently, Lambda Function and DynamoDB Table are supported.

Function

The Function wrapper annotates an aws_lambda.Function with a TypeScript function signature that controls how it can be called from within an AppsyncResolver.

import { aws_lambda } from "aws-cdk-lib";
import { Function } from "functionless";

const myFunc = new Function<{ name: string }, string>(
  new aws_lambda.Function(this, "MyFunc", {
    ..
  })
);

Within an AppsyncResolver, you can use the myFunc reference like an ordinary Function:

new AppsyncResolver(() => {
  return myFunc({ name: "my name" });
});

The first argument is passed to the Lambda Function.

{
  "name": "my name"
}

Table

The Table wrapper annotates an aws_dynamodb.Table with a type-safe interface that describes the Table's data.

See typesafe-dynamodb for more information on how to model DynamoDB Tables with TypeScript.

In short: you first declare an interface describing the data in your Table:

interface Item {
  key: string;
  data: number;
}

Then, wrap a aws_dynamodb.Table CDK Construct with the functionless.Table construct, specify the Item type, Partition Key "id" and (optionally) the Range Key.

import { aws_dynamodb } from "aws-cdk-lib";
import { Table } from "functionless";

// see https://github.com/sam-goodwin/typesafe-dynamodb for more information on type-safe DynamoDB Tables.
const myTable = new Table<Item, "key">(
  new aws_dynamodb.Table(this, "MyTable", {
    ..
  })
)

Finally, call getItem, putItem, etc. (see: #3) from within an AppsyncResolver:

new AppsyncResolver(() => {
  return myTable.get({
    key: $util.toDynamoDB("key"),
  });
});

AppsyncResolver construct for defining Appsync Resolver with TypeScript syntax

After wrapping your Functions/Tables, you can then instantiate an AppsyncResolver and interact with them using standard TypeScript syntax.

const getItem = new AppsyncResolver(
  ($context: AppsyncContext<{ key: string }>, key) => {
    const item = myTable.get({
      key: {
        S: key,
      },
    });

    const processedName = myFunc(item.key);

    return {
      ...item,
      processedName,
    };
  }
);

Calls to services such as Table or Function can only be performed at the top-level. See below for some examples of valid and invalid service calls

Valid:

// stash the result of the service call - the most common use-case
const item = myTable.get();

// calling the service but discarding the result is fine
myTable.get();

Invalid:

// you cannot in-line a call as the if condition, store it as a variable first
if (myTable.get()) {
}

if (condition) {
  // it is not currently possible to conditionally call a service, but this will be supported at a later time
  myTable.get();
}

for (const item in list) {
  // resolvers cannot be contained within a loop
  myTable.get();
}

Add Resolvers to an @aws-cdk/aws-appsync-alpha.GraphQLApi

When you create a new AppsyncResolver, it does not immediately generate an Appsync Resolver. AppsyncResolver is more like a template for creating resolvers and can be re-used across more than one API.

To add to an API, use the addResolver utility on AppsyncResolver.

const app = new App();

const stack = new Stack(app, "stack");

const schema = new appsync.Schema({
  filePath: path.join(__dirname, "..", "schema.gql"),
});

const api = new appsync.GraphqlApi(stack, "Api", {
  name: "demo",
  schema,
  authorizationConfig: {
    defaultAuthorization: {
      authorizationType: appsync.AuthorizationType.IAM,
    },
  },
  xrayEnabled: true,
});

// create a template AppsyncResolver
const getPerson = new AppsyncResolver(..);

// use it add resolvers to a GraphqlApi.
getPerson.addResolver(api, {
  typeName: "Query",
  fieldName: "getPerson",
});

Event Bridge

Functionless makes using Event Bridge easy by leveraging typescript instead of AWS Event Bridge's proprietary logic and transform configuration.

Event Bridge can:

  • Create Rules (EventBusRule) on a Event Bus to match incoming events.
  • Transform the event before sending to some services like Lambda Functions.
  • Target other AWS services like Lambda and other Event Buses
  • Put events from other services

Functionless uses a wrapped version of CDK's Event Bus, lets create a CDK event bus first.

// Create a new Event Bus using CDK.
const bus = new functionless.EventBus(this, "myBus");

// Functionless also supports using the default bus or importing an Event Bus.
const awsBus = functionless.EventBus.fromBus(
  new aws_events.EventBus(this, "awsBus")
);
const defaultBus = functionless.EventBus.fromBus(
  aws_events.EventBus.fromEventBusName(this, "defaultBus", "default")
);
const importedBus = functionless.EventBus.fromBus(
  aws_events.EventBus.fromEventBusArn(this, "defaultBus", arn)
);

Functionless supports well typed events, lets add our event schema to Typescript.

interface UserDetails {
  id?: string;
  name: string;
  age: number;
  interests: string[];
}

interface UserEvent
  extends functionless.EventBusRuleInput<
    UserDetails,
    // We can provide custom detail-types to match on
    "Create" | "Update" | "Delete"
  > {}

Create Rules (EventBusRule) on a Event Bus to match incoming events.

Now that you have a wrapped EventBus, lets add some rules.

Functionless lets you write logic in Typescript on the type safe event.

Lets match all of the Create or Update events with one rule and another rule for Deletes.

const createOrUpdateEvents = bus.when(
  this,
  "createOrUpdateRule",
  (event) =>
    event["detail-type"] === "Create" || event["detail-type"] === "Update"
);
const deleteEvents = bus.when(
  this,
  "deleteRule",
  (event) => event["detail-type"] === "Delete"
);

We also want to do something special when we get a new cat lover who is between 18 and 30 years old, lets make another rule for those.

const catPeopleEvents = bus.when(
  (event) =>
    event["detail-type"] === "Create" &&
    event.detail.interests.includes("CATS") &&
    event.detail.age >= 18 &&
    event.detail.age < 30
);

Rules can be further refined by calling when on a Functionless EventBusRule.

// Cat people who are between 18 and 30 and do not also like dogs.
catPeopleEvents.when(event => !event.detail.interests.includes("DOGS"))

Transform the event before sending to some services like Lambda Functions.

We have two lambda functions to invoke, one for create or updates and another for deletes, lets make those.

const createOrUpdateFunction = new aws_lambda.Function(this, 'createOrUpdate', ...);
const deleteFunction = new aws_lambda.Function(this, 'delete', ...);

and wrap them with Functionless's Function wrapper, including given them input types.

interface CreateOrUpdate {
  id?: string;
  name: string;
  age: number;
  operation: "Create" | "Update";
  interests: string[];
}

interface Delete {
  id: string;
}

const createOrUpdateOperation = functionless.Function<CreateOrUpdate, void>(
  createOrUpdateFunction
);
const deleteOperation = functionless.Function<Delete, void>(deleteFunction);

The events from before do not match the formats from before, so lets transform them to the structures match.

const createOrUpdateEventsTransformed =
  createOrUpdateEvents.map<CreateOrUpdate>((event) => ({
    id: event.detail.id,
    name: event.detail.name,
    age: event.detail.age,
    operation: event["detail-type"],
    interests: event.detail.interests,
  }));

const deleteEventsTransformed = createOrUpdateEvents.map<Delete>((event) => ({
  id: event.detail.id,
}));

Target other AWS services like Lambda and other Event Buses

Now that we have created rules on our event buses using when and transformed those matched events using map, we need to send the events somewhere.

We can pipe the transformed events to the lambda functions we defined earlier.

createOrUpdateEventsTransformed.pipe(createOrUpdateOperation);
deleteEventsTransformed.pipe(deleteOperation);

What about our young cat lovers? We want to forward those events to our sister team's event bus for processing.

const catPeopleBus = functionless.EventBus.fromBus(
  aws_events.EventBus.fromEventBusArn(this, "catTeamBus", catTeamBusArn)
);

// Note: EventBridge does not support transforming events which target other event buses. These events are sent as is.
catPeopleEvents.pipe(catPeopleBus);

Put Events from other sources

Event Bridge Put Events API is one of the methods for putting new events on an event bus. We support some first party integrations between services and event bus.

Support (See issues for progress):

  • Step Functions
  • App Sync (coming soon)
  • API Gateway (coming soon)
  • More - Please create a new issue in the form Event Bridge + [Service]
bus = new EventBus(stack, "bus");
new StepFunction<{ value: string }, void>((input) => {
  bus({
    detail: {
      value: input.value,
    },
  });
});

This will create a step function which sends an event. It is also possible to send multiple events and use other Step Function logic.

Limit: It is not currently possible to dynamically generate different numbers of events. All events sent must start from objects in the form { detail: ..., source: ... } where all fields are optional.

Summary

Lets look at the above all together.

interface UserDetails {
  id?: string;
  name: string;
  age: number;
  interests: string[];
}

interface UserEvent
  extends functionless.EventBusRuleInput<
    UserDetails,
    // We can provide custom detail-types to match on
    "Create" | "Update" | "Delete"
  > {}

interface CreateOrUpdate {
  id?: string;
  name: string;
  age: number;
  operation: "Create" | "Update";
  interests: string[];
}

interface Delete {
  id: string;
}

const createOrUpdateFunction = new functionless.Function<CreateOrUpdate, void>(
  new aws_lambda.Function(this, "createOrUpdate", { ... })
);

const deleteFunction = new functionless.Function<Delete, void>(
  new aws_lambda.Function(this, "delete", { ... })
);

const bus = new functionless.EventBus<UserEvent>(this, "myBus");

// Create and update events are sent to a specific lambda function.
bus
  .when(
    this,
    "createOrUpdateRule",
    (event) =>
      event["detail-type"] === "Create" || event["detail-type"] === "Update"
  )
  .map<CreateOrUpdate>((event) => ({
    id: event.detail.id,
    name: event.detail.name,
    age: event.detail.age,
    operation: event["detail-type"] as "Create" | "Update",
    interests: event.detail.interests,
  }))
  .pipe(createOrUpdateFunction);

// Delete events are sent to a specific lambda function.
bus
  .when(this, "deleteRule", (event) => event["detail-type"] === "Delete")
  .map<Delete>((event) => ({
    id: event.detail.id!,
  }))
  .pipe(deleteFunction);

// New, young users interested in cat are forwarded to our sister team.
bus
  .when(
    this,
    "catLovers",
    (event) =>
      event["detail-type"] === "Create" &&
      event.detail.interests.includes("CATS") &&
      event.detail.age >= 18 &&
      event.detail.age < 30
  )
  .pipe(
    functionless.EventBus<UserEvent>.fromBus(
      aws_events.EventBus.fromEventBusArn(this, "catTeamBus", catBusArn)
    )
  );

TypeScript -> Velocity Template Logic

In order to write effective VTL templates, it helps to understand how TypeScript syntax maps to Velocity Template Statements.

An AppSync Request Mapping Template is synthesized by evaluating all Expressions to a series of #set, $util.qr, #foreach and #if statements. The end result is an object containing the returned result of the function which can then be converted to JSON with $util.toJson.

The following section provides a reference guide on how each of the supported TypeScript syntax is mapped to VTL.

Click to expand

Parameter Reference

A reference to the top-level Function Parameter is mapped to a $context in VTL:

new AppsyncResolver((c: AppsyncContext<{ arg: string }>) => {
  return c.arguments.arg;
});
#return($context.arguments.arg)

Variable Declaration

If in the top-level scope, all Variables are stored in $context.stash.

new AppsyncResolver(() => {
  const a = "value";
  const b = a;
});
#set($context.stash.a = 'value')
#set($context.stash.b = $context.stash.a)

Variable Declaration in a nested scope

If in a nested scope, then the local variable name is used. These variables will not be available across Resolver Pipeline stages - but this should not be a problem as they are contained within a nested scope in TypeScript also.

new AppsyncResolver(() => {
  if (condition) {
    const a = "value";
    const b = a;
  }

  for (const i in list) {
    const a = "value";
    const b = a;
  }
});
#if($condition)
#set($a = 'value')
#set($b = $a)
#end

#foreach($i in $list)
#set($a = 'value')
#set($b = $a)
#end

Template Expressions (string interpolation)

Template expressions translate almost 1:1 with VTL:

const a = `hello ${name}`;
#set($context.stash.a = "hello ${name}")

Property and Index Assignment

a[0] = value;
a.prop = value;
a["prop"] = value;
a[prop] = value;
$util.qr($a[0] = $value)
$util.qr($a.prop = $value)
$util.qr($a['prop'] = $value)
$util.qr($a[$prop] = $value)

ArrayLiteralExpr

Array Literals can contain arbitrary expressions.

const a = [];
const b = ["hello", 1, util.toJson(a)];
#set($a = [])
#set($b = ['hello', 1, $util.toJson($a)])

SpreadElementExpr

There is a special case when you use a SpreadElementExpr (e.g. [...list]) because there is no way to achieve this behavior in VTL without first assigning a list and then using addAll to copy the items in.

If you ever use SpreadElementExpr, a temporary variable will be first initialized with an empty array ([]):

const c = [...b];
#set($v1 = [])
$util.qr($c.addAll($b))
#set($c = $v1)

ObjectLiteralExpr

An ObjectLiteralExpr is first stored as an empty map {} in a temporary variable and subsequent statements are generated to add each of the elements in.

const a = {
  key: "string",
};
#set($a = {})
$util.qr($a.put('key', 'string'))

SpreadAssignExpr

If you spread an object into another, a java.util.Map.putAll statement is generated to copy over each item in the source object into the destination object.

const a = {
  ...obj,
};
#set($a = {})
$util.qr($a.putAll($obj))

CallExpr - $util

The $util.* utility functions are translated verbatim into a VTL expression.

$util.error("error");
const a = $util.toJson(val);
$util.error('error')
#set($a = $util.toJson($val))

If Statement

An if statement translates to a series of #if, #else statements.

if (a === "hello") {
  return a;
}
#if($a == 'hello')
  #return($a)
#end

#elseif is not used because evaluating the condition may translate to a series of #set or $util.qr statements. For this reason, all else if clauses are translated to #else with a nested #if:

if (a === "hello") {
  return a;
} else if (call() === "hello") {
  return false;
}
#if($a == 'hello')
  #return($a)
#else
  #set($v1 = call())
  #if($v1 === "hello")
    #return($a)
  #end
#end

Conditional Expressions

A conditional expression, i.e. cond ? then : else are translated into #if and #else statements that assign a shared variable with the result of their computation;

const a = condition ? "left" : "right;
#if($condition)
#set($result = 'left')
#else
#set($result = 'right')
#end
#set($a = $result)

For-In-Statement

A for-in statement iterates over the keys in an object using java.util.Map.keySet().

for (const i in obj) {
  const a = obj[i];
}
#foreach($i in $obj.keySet())
#set($a = $obj[$i])
#end

For-Of-Statement

A for-of statement iterates over the items in a java.util.List.

for (const item in list) {
}
#foreach($item in $list)
#end

CallExpr - map

When you map over a list, a new list is created and then #foreach is used to iterate over the source list, evaluate your function and add the result to the new list.

Warning: chains of map, forEach and reduce results in redundant #foreach loops, see #2

const newList = list.map((i) => i + 1);
#set($newList = [])
#foreach($i in $list)
$util.qr($newList.add($i + 1))
#end

CallExpr - forEach

forEach is similar to map except it does not produce a value. The (below) example emulates map with forEach.

Warning: chains of map, forEach and reduce results in redundant #foreach loops, see #2

const newList = [];
list.forEach((i) => newList.push(i + 1));
#set($newList = [])
#foreach($i in $list)
$util.qr($newList.add($i + 1))
#end

CallExpr - reduce

reduce has two variants: 1) with an initialValue and 2) without.

Warning: chains of map, forEach and reduce results in redundant #foreach loops, see #2

If there is no initial value, then the list cannot be empty - if an empty list is encountered an error will be raised with $util.error.

Within the loop, the first value will not be processed by your function, instead it becomes the first value $a.

// without an initial value
const sum = list.reduce((a, b) => a + b);
#set(sum = [])
#if($list.isEmpty())
$util.error('Reduce of empty array with no initial value')
#end
#foreach($b in $list)
#if($foreach.index == 0)
#set($a = $b)
#else
#set($a = $a + $b)
#end
#end

If there is an initial value, then it is stored as a variable, referenced in the #foreach loop and overwritten at the end of each loop.

// with an initial value
const obj = list.reduce((a: Record<string, boolean>, b: string) => {
  ...a,
  [b]: true
}, {})
#set($a = {})
#foreach($b in $obj)
#set($v1 = {})
$util.qr($v1.putAll($a))
$util.qr($v1.put($b, true))
#set($a = $v1)
#end

Typescript -> Event patterns

Event patterns are all predicates that filter on the incoming event. The pattern is modeled as a predicate on the bus, resulting in a rule that follows the logic in the predicate.

https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html

.when(event => event.detail.value === "something")
Click to expand

Equals

.when(event => event.source === "lambda")
{
  "source": ["lambda"]
}

Not Equals

.when(event => event.source !== "lambda")
{
  "source": [{ "anything-but": "lambda" }]
}

Starts With

.when(event => event.source.startsWith("lambda"))
{
  "source": [{ "prefix": "lambda" }]
}

Not Starts With

.when(event => !event.source.startsWith("lambda"))
{
  "source": [{ "anything-but": { "prefix": "lambda" } }]
}

Limit: Anything-but Prefix cannot work with any other logic on the same field.

List Includes

.when(event => event.resources.includes("some arn"))
{
  "resources": ["some arn"]
}

Limit: Event Bridge patterns only support includes logic for lists, exact match and order based logic is not supported.

Numbers

.when(event => event.detail.age > 30 && event.detail.age <= 60)
{
  "detail": {
    "age": [{ "numeric": [">", 30, ",<=", 60] }]
  }
}

Non-converging ranges

.when(event => event.detail.age < 30 || event.detail.age >= 60)
{
  "detail": {
    "age": [{ "numeric": [">", 30] }, { "numeric": [">=", 60] }]
  }
}

Inversion

.when(event => !(event.detail.age < 30 && event.detail.age >= 60))
{
  "detail": {
    "age": [{ "numeric": [">=", 30, "<", 60] }]
  }
}

Reduction

.when(event => (event.detail.age < 30 || event.detail.age >= 60) &&
               (event.detail.age < 20 || event.detail.age >= 50) &&
               event.detail.age > 0)
{
  "detail": {
    "age": [{ "numeric": [">", 0, "<", 20] }, { "numeric": [">=", 60] }]
  }
}

Or Logic

Limit: Event Bridge patterns do not support OR logic between fields. The logic event.source === "lambda" || event['detail-type'] === "LambdaLike" is impossible within the same rule.

.when(event => event.source === "lambda" || event.source === "dynamo")
{
  "source": ["lambda", "dynamo"]
}

And Logic

Limit: Except for the case of numeric ranges and a few others Event Bridge does not support AND logic within the same field. The logic event.resources.includes("resource1") && event.resources.includes("resource2") is impossible.

.when(event => event.source === "lambda" && event.id.startsWith("idPrefix"))
{
  "source": ["lambda"],
  "id": [{ "prefix": "isPrefix" }]
}

Presence

Exists

.when(event => event.detail.optional !== undefined)
.when(event => !!event.detail.optional)
{
  "detail": {
    "optional": { "exists": true }
  }
}

Does not Exist

.when(event => event.detail.optional === undefined)
.when(event => !event.detail.optional)
{
  "detail": {
    "optional": { "exists": false }
  }
}

Simplification

.when(event => event.detail.optional && event.detail.optional === "value")
{
  "detail": {
    "optional": ["value"]
  }
}

Typescript -> Event Target Input Transformers

Event input transformers are pure functions that transform the input json into a json object or string sent to the target. The transformer is modeled as a map function.

https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html

Limit: Event Bridge does not support input transformation when sending data between buses.

Click to expand

Constant

.map(() => "got one!")
{
  "input": "got one!"
}

String field

.map(event => event.source)

Simple inputs can use eventPath.

{
  "inputPath": "$.source"
}

Formatted String

.map(event => `the source is ${event.source}`)
{
  "inputPathsMap": {
    "source": "$.source"
  },
  "inputTemplate": "the source is <source>"
}

Whole Event

.map(event => event)
{
  "inputPathsMap": {},
  "inputTemplate": "<aws.events.event>"
}

Rule Name and Rule Arn

.map((event, $utils) => `name: ${$utils.context.ruleName} arn: ${$utils.context.ruleArn}`)
{
  "inputPathsMap": {},
  "inputTemplate": "name: <aws.events.rule-name> arn: <aws.events.rule-arn>"
}

Constant Objects

.map(event => event.detail)
{
  "inputPath": "$.detail"
}

Objects

.map(event => ({
  value: event.detail.field,
  source: event.source,
  constant: "hello"
}))
{
  "inputPathsMap": {
    "field": "$.detail.field",
    "source": "$.source"
  },
  "inputTemplate": "{ \"value\": <field>, \"source\": <source>, \"constant\": \"hello\" }"
}

How it Works

When you compile your application with tsc, the functionless/lib/compile transformer will replace the function declaration, F, in new AppsyncResolver(F) with its corresponding Abstract Syntax Tree representation. This representation is then synthesized to Velocity Templates and AWS AppSync Resolver configurations, using the @aws-cdk/aws-appsync-alpha CDK Construct Library.

For example, this function declaration:

new AppsyncResolver<(input: { name: string }) => Person>((_$context, input) => {
  const person = this.personTable.putItem({
    key: {
      id: {
        S: $util.autoId(),
      },
    },
    attributeValues: {
      name: {
        S: input.name,
      },
    },
  });

  return person;
});

Is replaced with the following AST data structure:

new AppsyncResolver(
  new FunctionDecl(
    [new ParameterDecl("input")],
    new BlockStmt([
      new VariableStmt(
        "person",
        new CallExpr(
          new PropAccessExpr(
            new ReferenceExpr(() => this.personTable),
            "putItem"
          ),
          {
            input: new ObjectLiteralExpr([
              new PropAssignExpr(
                "key",
                new ObjectLiteralExpr([
                  new PropAssignExpr(
                    "id",
                    new ObjectLiteralExpr([
                      new PropAssignExpr(
                        "S",
                        new CallExpr(
                          new PropAccessExpr(new Identifier("$util"), "autoId"),
                          {}
                        )
                      ),
                    ])
                  ),
                ])
              ),
              new PropAssignExpr(
                "attributeValues",
                new ObjectLiteralExpr([
                  new PropAssignExpr(
                    "name",
                    new ObjectLiteralExpr([
                      new PropAssignExpr(
                        "S",
                        new PropAccessExpr(new Identifier("input"), "name")
                      ),
                    ])
                  ),
                ])
              ),
            ]),
          }
        )
      ),
      new ReturnStmt(new Identifier("person")),
    ])
  )
);

Writing your own interpreters

Functionless converts TypeScript function syntax into a FunctionDecl AST data object. This object contains a total representation of the syntax contained within the Function and can then be processed within your CDK application.

To get a FunctionDecl for a function, use the functionless.reflect utility:

import { reflect } from "functionless";

const functionDecl = reflect((arg: string) => {
  return `${arg}_1`;
});

Then, write a recursive function to process the representation:

import { FunctionlessNode } from "functionless";

function processExpr(node: FunctionlessNode) {
  // do work
  if (node.kind === "FunctionDecl") {
    // blah
  }
}

See the following files to understand the structure of the Abstract Syntax Tree:

  1. expression.ts
  2. statement.ts
  3. declaration.ts

For an example of an evaluator, see vtl.ts.

Generating resolver types from the schema

Functionless can be used together with graphql code generator to automatically generate types from the schema.

Two plugins are necessary to generate resolver types:

Both of those plugins need to be configured by creating a codegen.yml file.

overwrite: true
schema:
  # The path to your schema
  - "schema.gql"
generates:
  # path to the file with the generated types
  src/generated-types.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
    config:
      # Set to true in order to allow the Resolver type to be callable
      makeResolverTypeCallable: true
      # This will cause the generator to avoid using optionals (?), so all field resolvers must be implemented in order to avoid compilation errors
      avoidOptionals: true
      # custom type for the resolver makes it easy to reference arguments, source and result from the resolver
      customResolverFn: "{ args: TArgs; context: TContext; result: TResult; source: TParent;}"
      # appsync allows returnning undefined instead of null only when a type is optional
      maybeValue: T | null | undefined
      # typename is not really usefull for resolvers and can cause clashes in the case where a type extends another type but have different names
      skipTypename: true

you can then use npx graphql-codegen --config codegen.yml to generate a file containing the types, you should re-generate them any time you update your schema.

If you use the following schema

type Person {
  id: String!
  name: String!
}

type Query {
  getPerson(id: String!): ProcessedPerson
}

The generated types will include type definitions for all graphql types, inputs and resovlers. Those types can then be imported in your cdk app.

import { QueryResolvers, Person } from "./generated-types";
import { $util, AppsyncResolver } from "functionless";

export class PeopleDatabase extends Construct {
  readonly personTable;
  readonly getPerson;

  constructor(scope: Construct, id: string) {
    super(scope, id);
    // Person type can be used to define your typesafe dynamodb table
    this.personTable = new Table<Person, "id", undefined>(
      new aws_dynamodb.Table(this, "table", {
        partitionKey: {
          name: "id",
          type: aws_dynamodb.AttributeType.STRING,
        },
      })
    );
    // QueryResolvers type can be used to get parameters for AppsyncResolver
    this.getPerson = new AppsyncResolver<
      QueryResolvers["addPerson"]["args"],
      QueryResolvers["addPerson"]["result"]
    >(($context) => {
      const person = this.personTable.putItem({
        key: {
          id: {
            S: $util.autoId(),
          },
        },
        attributeValues: {
          name: {
            S: $context.arguments.input.name,
          },
        },
      });

      return person;
    });
  }
}

Check the test-app for a full working example.

Comments
  • Is it possible to use a `Function` as a lambda authorizer?

    Is it possible to use a `Function` as a lambda authorizer?

    Here is what I'm trying to do:

    const defaultAuthFunction0 = new Function(
      this,
      'LambdaAuthorizer',
      {
        memorySize: 256,
        timeout: Duration.minutes(1),
        logRetention: 14
      },
      async () => ({isAuthorized: true})
    );
    const defaultAuthFunction = defaultAuthFunction0.resource;
    
    
    const api = new appsync.GraphqlApi(this, 'API', {
      name: 'API',
      schema,
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: appsync.AuthorizationType.LAMBDA,
          lambdaAuthorizerConfig: {
            handler: defaultAuthFunction,
          },
        },
      },
      xrayEnabled: true,
      logConfig: {
        fieldLogLevel: FieldLogLevel.ALL,
      },
    });
    

    But I'm getting a SynthError

    SynthError: Expected input function to Function to be compiled by Functionless. Make sure you have the Functionless compiler plugin configured correctly.
    
    opened by volkanunsal 35
  • Support for typesafe SDK integrations

    Support for typesafe SDK integrations

    It would be great to have general SDK integrations (beyond just DynamoDB and EventBridge) similar to ts2asl.

    Maybe something like

    import { S3 } from "@aws-sdk/client-s3"; // use the v2 compatible to infer types
    
    const workflow = new StepFunction(
      this,
      "Workflow",
      async (input: Input) => {
        await $AWS.SDK(S3).getObject({
          Bucket: input.bucket,
          Key: input.key
        });
      }
    );
    

    the v2 compatible style has all functions directly on the class which would allow to infer the type

    enhancement 
    opened by matthias-pichler-warrify 29
  • AWS Event Bridge

    AWS Event Bridge

    • [x] Operate on a type safe event
    • [ ] Define rule
      • [x] Using predicate on the event
      • [ ] Using conditionals inline with targets
      • [ ] Using an existing CDK rule
    • [x] Define function to transform event to target service
      • [ ] Using conditionals for rules and many targets
      • [x] Using standalone targets for a single rule
      • [ ] Integrations
        • [x] Event Bridge
        • [ ] AppSync - https://aws.amazon.com/blogs/mobile/appsync-eventbridge/
        • [ ] ApiGateway #23
        • [ ] Step Functions #7
        • [x] Function
        • [ ] Dynamo
    • [ ] Event bus as service target to publish events
      • [ ] From Step Functions #7
      • [ ] From AppSync - https://github.com/awsed/AppSync2EventBridge/blob/master/index.ts
      • [ ] From API Gateway #23

    Option 1: As Rule and Target

    construct that accepts a bus and a predicate, returns a rule.

    interface MyEventDetail {
        value: string 
    }
    type MyEvent = EventBusEvent<MyEventDetail, "my-event-detail-type">;
    
    const rule = new EventBridgeRule<MyEvent>(bus, (event): boolean => {
       return event.source == 'lambda' && event.detail.value.startsWith('something');
    })
    const function = new Function(...);
    const target = new EventBridgeTarget<MyEvent>(rule, (event) => {
        function(event.detail)
    });
    

    Option 2: As Single Integration

    interface MyEventDetail {
        value: string 
    }
    type MyEvent = EventBusEvent<MyEventDetail, "my-event-detail-type">;
    
    const function = new Function(...);
    new EventBus<MyEvent>(bus, (event, $util) => {
        if($util.matchType<MyEvent>()) {
              function(event.detail);
        }
    })
    

    As AppSync target

    import {aws_events} from 'aws-cdl-lib';
    import { EventBus } from 'functionless';
    
    cosnt eventBus = new EventBus(new aws_events.EventBus());
    
    const postEvent = new AppsyncResolver<{ id: string }, Item | null>(($context) => {
        eventBus.putEvent($context.arguments);
    })
    
    event-bridge 
    opened by thantos 24
  • feat: sign-in page, icons, and styling

    feat: sign-in page, icons, and styling

    Closes #189 Part of #204

    • Hubspot signup form on /sign-up
    • FunctionlessHighlighter
    • Start using material UI (MUI)
    • theming and dark mode
    • Updated Icon(s)
    documentation website 
    opened by thantos 18
  • Outdated @aws-cdk/aws-appsync-alpha dependency in package.json

    Outdated @aws-cdk/aws-appsync-alpha dependency in package.json

    functionless version 0.6:

    Using the latest "@aws-cdk/aws-appsync-alpha": "2.21.1-alpha.0" package in project gives:

    npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve npm ERR! npm ERR! While resolving: [email protected] npm ERR! Found: @aws-cdk/[email protected] npm ERR! node_modules/@aws-cdk/aws-appsync-alpha npm ERR! @aws-cdk/aws-appsync-alpha@"^2.21.1-alpha.0" from the root project npm ERR! npm ERR! Could not resolve dependency: npm ERR! peer @aws-cdk/aws-appsync-alpha@"^2.17.0-alpha.0" from [email protected] npm ERR! node_modules/functionless npm ERR! dev functionless@"^0.6.0" from the root project npm ERR! npm ERR! Conflicting peer dependency: @aws-cdk/[email protected] npm ERR! node_modules/@aws-cdk/aws-appsync-alpha npm ERR! peer @aws-cdk/aws-appsync-alpha@"^2.17.0-alpha.0" from [email protected] npm ERR! node_modules/functionless npm ERR! dev functionless@"^0.6.0" from the root project npm ERR! npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force, or --legacy-peer-deps npm ERR! to accept an incorrect (and potentially broken) dependency resolution. npm ERR!

    It seems that dev or peerDependecies is creation this error. Would be great if it can be upgraded or more flexible.

    opened by hfahlbusch 16
  • SQS Queue type-safe integration class

    SQS Queue type-safe integration class

    Provide a wrapper class, Queue<T>, that models the SQS Queue Functionless integrations.

    Must integrates with the following:

    1. AppSync
    const queue = new Queue<string>(new aws_sqs.Queue(..))
    
    new AppsyncResolver<{id: string}>($context => {
      queue.sendMessage({
         MessageBody: id,
         MessageAttributes: ..
      });
    });
    
    1. Step Functions
    new StepFunction(scope, id, (id: string) => {
      queue.sendMessage({
         MessageBody: id,
         MessageAttributes: ..
      })
    })
    
    1. Event Bridge
    bus
      .when(scope, "rule", event => event.source === "lambda")
      .map(event => event.message)
      .pipe(queue)
    
    1. Lambda
    new Function(scope, id, async () => {
      await queue.sendMessage(..);
    });
    
    sqs 
    opened by sam-goodwin 16
  • table.query is not a function

    table.query is not a function

    I tried using Table inside a Function, and got this error. The generated code seems to be missing every method. Did I do this right?

    ddbTable.addGlobalSecondaryIndex({
      indexName: 'GSI1',
      partitionKey: { name: 'PK', type: aws_dynamodb.AttributeType.STRING },
      sortKey: { name: 'SK', type: aws_dynamodb.AttributeType.STRING },
      projectionType: aws_dynamodb.ProjectionType.ALL,
    });
    
    const table = new Table<ITable, 'PK', any>(ddbTable);
    
    const defaultAuthFunction = new Function<AuthContext, AuthResponse>(
      this,
      'LambdaAuthorizer',
      {
        logRetention: 14,
      },
      async (c) => {
        const apiKey = c.authorizationToken;
    
        // Look up the account using the auth token.
        const item = table.query({
          index: 'GSI1',
          query: {
            expression: 'PK = :pk',
            expressionValues: { ':pk': { S: apiKey } },
          },
        }).items[0];
    
        // If it exists and it's a valid account, allow request.
        if (item?.entityType === 'Account') {
          const accountId = item.Id;
          return {
            isAuthorized: true,
            // Add account id and api key into the resolver context.
            resolverContext: { accountId, apiKey },
            deniedFields: [],
          };
        }
    
        //
        return {
          isAuthorized: false,
          resolverContext: {},
          deniedFields: [],
        };
      },
    );
    

    And here is the part of the generated code with the __table object.

    var __table = {};
    var __table_query = {};
    __table_query.kind = "Table.query";
    __table_query.unhandledContext = __f5;
    var __table_query_native = {call: undefined, preWarm: undefined};
    __table_query.native = __table_query_native;
    __table.query = __table_query;
    
    opened by volkanunsal 15
  • feat: native typescript lambda function code

    feat: native typescript lambda function code

    Closes #153

    Depends on #109 and #108

    Function closure serialization!

    • [x] Basic Function serialization
    • [x] Add constructor to Function to pass in a closure, scope, and id
    • [x] ~~Exhaustive~~ tests
    • [x] Support embedded client calls
      • [x] Step Functions
      • [x] Lambda
      • ~~App Sync~~ include in #119
      • ~~Api Gateway Endpoints~~ Not implemented - include in #23
      • [x] Event Bridge
      • [x] Dynamo
        • Only within $AWS.DynamoDB.* see #132
    • [x] Support arbitrary token data as env variables
    • [x] Split localstack integration into a separate PR #109
    • [x] AppSync integration changes #113
    • Dynamically simplify native integration references #122
    new Function(this, 'myFunction', async (event) => {
       return Object.keys(event).length;
    })
    
    opened by thantos 15
  • AppsyncFunction Interface

    AppsyncFunction Interface

    I love this project and I've been trying to use it in addition with graphql codegen to generate types automatically from the schema. While trying to do this I've had issues with the API for the AppsyncFunction class

    The issue I see is that the name of the function parameter influences the result of the mapping template so if I use the code from the example app This works

        this.getPerson = new AppsyncFunction<
          (id: string) => ProcessedPerson | null
        >((_$context, id) => {
          const person = this.personTable.getItem({
            key: {
              id: $util.dynamodb.toDynamoDB(id),
            },
            consistentRead: true,
          });
    

    But this doesn't work because the name of the parameter is not the same as the name of the graphql query parameter

        this.getPerson = new AppsyncFunction<
          (id: string) => ProcessedPerson | null
        >((_$context, userId) => { // notice the argument name has changed
          const person = this.personTable.getItem({
            key: {
              id: $util.dynamodb.toDynamoDB(userId),
            },
            consistentRead: true,
          });
    

    This means that no amount of code generation will help here as even if we successfully generate a type that looks like (id: string) => ProcessedPerson | null but don't use the right parameter name then you will not see a typescript error there.

    Ideally I think that the interface for this function could look something like

    AppsyncFunction<TArgs, TResult, TSource = undefined> {
        readonly decl: FunctionDecl<F>;
        constructor(fn: ($context: $Context<Source>, args: TArgs) => TResult);
    

    Then the arguments would be used with args.id so slightly less good looking but with destructuring the difference is small, but this has the benefit of better type safety as using args.userId would result in a typescript error.

    As an added benefit this would make using functionless with graphql codegen typescript resolver plugin much easier

    opened by sho-ai-main 15
  • Step Functions interpreter

    Step Functions interpreter

    Similar to AppsyncFunction, we should build interpreters for AWS Step Functions:

    1. new ExpressStepFunction
    2. new StandardStepFunction

    The ExpressStepFunction represents a synchronous workflow that returns a value and has a limited subset of features available (see: https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html#welcome-workflows)

    const getPerson = new ExpressStepFunction((arg: string): Person | undefined => {
      const person = personTable.get(arg);
      return {
        id: person.id.S,
        name: person.name.S
       };
    })
    

    The StandardStepFunction represents an asynchronous workflow that does not return a value - to retrieve the value, a caller must call getExecutionStatus.

    Appsync Integration

    It should be possible to call a Step Function from within an Appsync Resolver:

    new AppsyncFunction(() => {
      // ExpressStepFunction returns synchronously
      const person = getPerson("arg");
    })
    
    opened by sam-goodwin 15
  • feat: generalize all app sync integration hooks

    feat: generalize all app sync integration hooks

    Closes #117

    Previous, much of the integration logic for AppSync were in the app sync logic. This change moves all of the per integration (ex: app sync calling lambda) into the specific integrations (ex: out of app sync file and into the Function file).

    Changes:

    • Change the vtl integration into appSyncVtl as the integrations are specific to AppSync and not other VTL implementations (Api Gateway)
    • Change the vtl/appSyncVtl integration from a single function to 3 methods.
      • request - the main request vtl template
      • result - a post request processor that can transform the results from the service call (used by step functions now)
      • dataSource - retrieve a CDK data source for the integration
    • Use CDK's tryFindChild to resolve hierarchical singleton constructs instead of a separate map.
    • Add a static kind to each of the integration types so that they may be found by the compiler. Previously, only the top level objects (Function, StepFunction, Table) had kind. Now each of the integrations will have a kind (ex: StepFunction.describeExecution)
    • Deleted things
    • Compile: Only add functionless import when necessary.
    • Improve the creation of localstack tests, reduce boilerplate
    • fix: format function names for app sync
    • feat: Added getField to use Functionless AppSync Resolver with CodeFirst
      • Note: a larger change is planned with #119 to refactor app sync, however this change adds some more capability without changing the interface.
      • [ ] Update Docs

    Code First App Sync Resolver

    const commentResolver = new functionless.AppSyncResolver(...);
    const getPostResolver = new functionless.AppSyncResolver(...);
    
    /*
      type Query {
        getPost(postId: string!): Post
      } 
    
     type Post {
      postId: ID!
      title: String!
      comments(nextToken: String, limit: Int): CommentPage
     }
    
     type CommentPage {
      nextToken: String
      comments: [Comment]!
     }
    
     type Comment {
      postId: ID!
      commentId: ID!
      commentText: String!
      createdTime: String!
     }
     */
    
    const post = api2.addType(
      new appsync.ObjectType("Post", {
        definition: {
          postId: appsync.GraphqlType.id({
            isRequired: true,
          }),
          title: appsync.GraphqlType.string({
            isRequired: true,
          }),
        },
      })
    );
    
    const comment = api2.addType(
      new appsync.ObjectType("Comment", {
        definition: {
          postId: appsync.GraphqlType.id({
            isRequired: true,
          }),
          commentId: appsync.GraphqlType.id({
            isRequired: true,
          }),
          commentText: appsync.GraphqlType.string({
            isRequired: true,
          }),
          createdTime: appsync.GraphqlType.string({
            isRequired: true,
          }),
        },
      })
    );
    
    const commentPage = api2.addType(
      new appsync.ObjectType("CommentPage", {
        definition: {
          nextToken: appsync.GraphqlType.string(),
          comments: appsync.GraphqlType.intermediate({
            intermediateType: comment,
            isRequiredList: true,
          }),
        },
      })
    );
    
    post.addField({
      fieldName: "comments",
      field: commentResolver.getField(api2, commentPage.attribute(), {
        args: {
          nextToken: appsync.GraphqlType.string(),
          limit: appsync.GraphqlType.int(),
        },
      }),
    });
    
    api2.addQuery(
      "getPost",
      getPostResolver.getField(api2, post.attribute(), {
        args: {
          postId: appsync.GraphqlType.string({ isRequired: true }),
        },
      })
    );
    
    opened by thantos 14
  • chore: move website to functionless/website repo

    chore: move website to functionless/website repo

    Website build was taking too long and we received complaints that it crashed a lot. I moved the website to https://github.com/functionless/website and removed the API reference docs so it now builds fast.

    This change deletes the code from this repo and removes the build step.

    opened by sam-goodwin 0
  • feat: formation

    feat: formation

    V0.1 of @functionless/formation

    • [x] Modules Hanlders
    • [ ] Hand Rolled Resources (not supported by CC)
      • [ ] Event Bus
        • [x] Create
        • [ ] Update
        • [ ] Delete
      • [ ] Event Bus Rule (not supported by CC)
        • [x] Create
        • [ ] Update
        • [ ] Delete
      • [ ] Policy (not supported by CC)
        • [x] Create
        • [x] Update
        • [ ] Delete
      • [ ] Managed policy (not supported by CC)
        • [x] Create
        • [x] Update
        • [ ] Delete
      • [ ] Queue Policy (not supported by CC)
        • [x] Create
        • [x] Update
        • [ ] Delete
      • [ ] Queue (optimization)
        • [x] Create
        • [ ] Update
        • [ ] Delete
      • [ ] Role (optimization)
        • [x] Create
        • [ ] Update
        • [ ] Delete
      • [ ] Secret (not supported by CC)
        • [x] Create
        • [ ] Update
        • [ ] Delete
      • [ ] Event Source Mapping (optimization)
        • [x] Create
        • [ ] Update
        • [ ] Delete
      • [ ] more...
    • [ ] All Functionless Runtime Tests pass with Formless
      • [x] Lambda
      • [x] Queue - Event Bus Rule was broken
      • [ ] Sfn
      • [ ] Secret
      • [ ] Table
      • [ ] Api
    • [ ] Plan
      • [ ] Compute
        • [x] Assets
        • [x] Conditions
        • [x] Deletes
        • [ ] Create/Update
          • [ ] No-Op Updates
      • [ ] Render (cli)
    • [ ] Improve CLI Output
    • [ ] Support Delete
    • [ ] Support Statefulness via tags and or stateful saving
    • [ ] Support parameters
      • [ ] As dependencies
      • [ ] In cli
    • [ ] Bi-furcate consistent vs referenceable completion
    • [ ] Metrics
      • [x] Basic (per resource process time, n count, average)
      • [ ] Record
      • [ ] trace logs - https://github.com/wix-incubator/trace-event-lib
    • [ ] Support correct Ref logic for all resources - some reasons do not use ARN for REF like Queue (uses QueueUrl)
    • [ ] create external package and cli with a really clever name
    formation 
    opened by thantos 4
  • create-functionless-app: Created app cannot synth when using npm

    create-functionless-app: Created app cannot synth when using npm

    Synth fails when project is created/deps installed in npm, though not through yarn or pnpm npm cdk synth results in:

    
    thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: LayoutError', /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/rkyv-0.7.37/src/impls/core/mod.rs:265:67
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    thread '<unnamed>' panicked at 'failed to invoke plugin: failed to invoke plugin on 'Some("/home/chris/functionless/new-project/src/app.ts")'
    
    Caused by:
        0: failed to invoke `@functionless/ast-reflection` as js transform plugin at node_modules/@functionless/ast-reflection/ast_reflection.wasm
        1: RuntimeError: unreachable
               at __rust_start_panic (<module>[1581]:0x1470e8)
               at rust_panic (<module>[1574]:0x146e2d)
               at std::panicking::rust_panic_with_hook::h34503c79e5f975a0 (<module>[1573]:0x146daa)
               at std::panicking::begin_panic_handler::{{closure}}::h12a46dd1ec9ad9ef (<module>[1560]:0x146010)
               at std::sys_common::backtrace::__rust_end_short_backtrace::hbfd28f296b351783 (<module>[1559]:0x145f4f)
               at rust_begin_unwind (<module>[1568]:0x1466fa)
               at core::panicking::panic_fmt::h7092b9385b17ba68 (<module>[1666]:0x14d0d1)
               at core::result::unwrap_failed::hcf4dc3db6a31deee (<module>[1705]:0x14eb63)
               at rkyv::impls::core::<impl rkyv::DeserializeUnsized<[U],D> for [T]>::deserialize_unsized::h2f51fdec64c09b49 (<module>[951]:0xfdb43)
               at rkyv::impls::core::<impl rkyv::DeserializeUnsized<[U],D> for [T]>::deserialize_unsized::h2e8a8069be0c9f2f (<module>[399]:0x62b98)
               at swc_common::plugin::serialized::PluginSerializedBytes::deserialize::hf5e85413ebd1458b (<module>[96]:0x1995e)
               at swc_common::plugin::serialized::deserialize_from_ptr::h5296ff6c7163ba5e (<module>[95]:0x196ef)
               at __transform_plugin_process_impl (<module>[525]:0x86526)
               at __transform_plugin_process_impl.command_export (<module>[1766]:0x152883)
        2: unreachable', /usr/local/cargo/registry/src/github.com-1ecc6299db9ec823/swc-0.228.0/src/plugin.rs:228:14
    stack backtrace:
       0:     0x7f7e0225b19d - <unknown>
       1:     0x7f7e0228334c - <unknown>
       2:     0x7f7e02256a11 - <unknown>
       3:     0x7f7e0225cac5 - <unknown>
       4:     0x7f7e0225c7e6 - <unknown>
       5:     0x7f7e0225d056 - <unknown>
       6:     0x7f7e0225cf47 - <unknown>
       7:     0x7f7e0225b654 - <unknown>
       8:     0x7f7e0225cc79 - <unknown>
       9:     0x7f7e00602233 - <unknown>
      10:     0x7f7e00602323 - <unknown>
      11:     0x7f7e00dd483e - <unknown>
      12:     0x7f7e00ee26ab - <unknown>
      13:     0x7f7e00de1ebd - <unknown>
      14:     0x7f7e00ddb538 - <unknown>
      15:     0x7f7e008943dc - <unknown>
      16:     0x7f7e00896375 - <unknown>
      17:     0x7f7e007b546f - <unknown>
      18:     0x7f7e00888a01 - <unknown>
      19:     0x7f7e008005de - <unknown>
      20:           0xaaf64d - _ZN6v8impl12_GLOBAL__N_123FunctionCallbackWrapper6InvokeERKN2v820FunctionCallbackInfoINS2_5ValueEEE
      21:           0xd396ae - _ZN2v88internal12_GLOBAL__N_119HandleApiCallHelperILb0EEENS0_11MaybeHandleINS0_6ObjectEEEPNS0_7IsolateENS0_6HandleINS0_10HeapObjectEEESA_NS8_INS0_20FunctionTemplateInfoEEENS8_IS4_EENS0_16BuiltinArgumentsE
      22:           0xd3aacf - _ZN2v88internal21Builtin_HandleApiCallEiPmPNS0_7IsolateE
      23:          0x15d5519 - Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit
    /home/chris/functionless/new-project/node_modules/@swc/core/index.js:241
                return bindings.transformSync(isModule ? JSON.stringify(src) : src, isModule, toBuffer(newOptions));
                                ^
    
    Error: failed to handle: failed to invoke plugin: failed to invoke plugin on 'Some("/home/chris/functionless/new-project/src/app.ts")'
    
    Caused by:
        0: failed to invoke `@functionless/ast-reflection` as js transform plugin at node_modules/@functionless/ast-reflection/ast_reflection.wasm
        1: RuntimeError: unreachable
               at __rust_start_panic (<module>[1581]:0x1470e8)
               at rust_panic (<module>[1574]:0x146e2d)
               at std::panicking::rust_panic_with_hook::h34503c79e5f975a0 (<module>[1573]:0x146daa)
               at std::panicking::begin_panic_handler::{{closure}}::h12a46dd1ec9ad9ef (<module>[1560]:0x146010)
               at std::sys_common::backtrace::__rust_end_short_backtrace::hbfd28f296b351783 (<module>[1559]:0x145f4f)
               at rust_begin_unwind (<module>[1568]:0x1466fa)
               at core::panicking::panic_fmt::h7092b9385b17ba68 (<module>[1666]:0x14d0d1)
               at core::result::unwrap_failed::hcf4dc3db6a31deee (<module>[1705]:0x14eb63)
               at rkyv::impls::core::<impl rkyv::DeserializeUnsized<[U],D> for [T]>::deserialize_unsized::h2f51fdec64c09b49 (<module>[951]:0xfdb43)
               at rkyv::impls::core::<impl rkyv::DeserializeUnsized<[U],D> for [T]>::deserialize_unsized::h2e8a8069be0c9f2f (<module>[399]:0x62b98)
               at swc_common::plugin::serialized::PluginSerializedBytes::deserialize::hf5e85413ebd1458b (<module>[96]:0x1995e)
               at swc_common::plugin::serialized::deserialize_from_ptr::h5296ff6c7163ba5e (<module>[95]:0x196ef)
               at __transform_plugin_process_impl (<module>[525]:0x86526)
               at __transform_plugin_process_impl.command_export (<module>[1766]:0x152883)
        2: unreachable
        at Compiler.transformSync (/home/chris/functionless/new-project/node_modules/@swc/core/index.js:241:29)
        at Object.transformSync (/home/chris/functionless/new-project/node_modules/@swc/core/index.js:348:21)
        at compile (/home/chris/functionless/new-project/node_modules/@swc/register/lib/node.js:76:22)
        at compileHook (/home/chris/functionless/new-project/node_modules/@swc/register/lib/node.js:91:16)
        at Module._compile (/home/chris/functionless/new-project/node_modules/pirates/lib/index.js:130:29)
        at Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
        at Object.newLoader [as .ts] (/home/chris/functionless/new-project/node_modules/pirates/lib/index.js:141:7)
        at Module.load (node:internal/modules/cjs/loader:1004:32)
        at Function.Module._load (node:internal/modules/cjs/loader:839:12)
        at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) {
      code: 'GenericFailure'
    }
    

    Attached package-lock.json from npm, and pnpm-local.yaml from pnpm for comparison package-lock.json.txt pnpm-lock.yaml.txt

    output of pnpm ls --depth 1:

    @aws-cdk/aws-appsync-alpha 2.43.1-alpha.0
    ├── aws-cdk-lib 2.43.1 peer
    └── constructs 10.1.116 peer
    @functionless/ast-reflection 0.3.2
    └── @swc/core 1.3.3 peer
    @functionless/language-service 0.0.4
    └── functionless 0.23.6 peer
    @types/jest 29.1.0
    ├── expect 29.1.0
    └── pretty-format 29.1.0
    @types/node 18.7.23
    aws-cdk 2.43.1
    └── fsevents 2.3.2
    aws-cdk-lib 2.43.1
    └── constructs 10.1.116 peer
    aws-sdk 2.1225.0
    ├── buffer 4.9.2
    ├── events 1.1.1
    ├── ieee754 1.1.13
    ├── jmespath 0.16.0
    ├── querystring 0.2.0
    ├── sax 1.2.1
    ├── url 0.10.3
    ├── util 0.12.4
    ├── uuid 8.0.0
    └── xml2js 0.4.19
    constructs 10.1.116
    esbuild 0.15.9
    ├── @esbuild/android-arm 0.15.9
    ├── @esbuild/linux-loong64 0.15.9
    ├── esbuild-android-64 0.15.9
    ├── esbuild-android-arm64 0.15.9
    ├── esbuild-darwin-64 0.15.9
    ├── esbuild-darwin-arm64 0.15.9
    ├── esbuild-freebsd-64 0.15.9
    ├── esbuild-freebsd-arm64 0.15.9
    ├── esbuild-linux-32 0.15.9
    ├── esbuild-linux-64 0.15.9
    ├── esbuild-linux-arm 0.15.9
    ├── esbuild-linux-arm64 0.15.9
    ├── esbuild-linux-mips64le 0.15.9
    ├── esbuild-linux-ppc64le 0.15.9
    ├── esbuild-linux-riscv64 0.15.9
    ├── esbuild-linux-s390x 0.15.9
    ├── esbuild-netbsd-64 0.15.9
    ├── esbuild-openbsd-64 0.15.9
    ├── esbuild-sunos-64 0.15.9
    ├── esbuild-windows-32 0.15.9
    ├── esbuild-windows-64 0.15.9
    └── esbuild-windows-arm64 0.15.9
    functionless 0.23.6
    ├── @aws-cdk/aws-appsync-alpha 2.43.1-alpha.0 peer
    ├── @functionless/ast-reflection 0.3.2
    ├── @functionless/nodejs-closure-serializer 0.1.2
    ├── @swc/cli 0.1.57
    ├── @swc/core 1.2.245
    ├── @swc/register 0.1.10
    ├── @types/aws-lambda 8.10.106
    ├── aws-cdk-lib 2.43.1 peer
    ├── aws-sdk 2.1225.0 peer
    ├── constructs 10.1.116 peer
    ├── esbuild 0.15.9 peer
    ├── fs-extra 10.1.0
    ├── minimatch 5.1.0
    ├── source-map 0.7.4
    ├── typesafe-dynamodb 0.2.3 peer
    └── typescript 4.8.4 peer
    jest 29.1.1
    ├── @jest/core 29.1.1
    ├── @jest/types 29.1.0
    ├── import-local 3.1.0
    └── jest-cli 29.1.1
    typesafe-dynamodb 0.2.3
    ├── @aws-sdk/client-dynamodb 3.180.0
    ├── @aws-sdk/lib-dynamodb 3.180.0
    ├── @aws-sdk/smithy-client 3.180.0
    ├── @aws-sdk/types 3.178.0
    ├── @aws-sdk/util-dynamodb 3.180.0
    ├── @types/aws-lambda 8.10.106
    └── aws-sdk 2.1225.0
    typescript 4.8.4
    

    Output of npm ls --depth 1:

    [email protected] /home/chris/functionless/new-project
    ├─┬ @aws-cdk/[email protected]
    │ ├── [email protected] deduped
    │ └── [email protected] deduped
    ├─┬ @functionless/[email protected]
    │ └── @swc/[email protected]
    ├─┬ @functionless/[email protected]
    │ └── [email protected] deduped invalid: "^0.6.20" from node_modules/@functionless/language-service
    ├─┬ @types/[email protected]
    │ ├── [email protected]
    │ └── [email protected]
    ├── @types/[email protected]
    ├─┬ [email protected]
    │ ├── @balena/[email protected]
    │ ├── [email protected]
    │ ├── [email protected] deduped
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ └── [email protected]
    ├─┬ [email protected]
    │ └── UNMET OPTIONAL DEPENDENCY [email protected]
    ├─┬ [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ └── [email protected]
    ├── [email protected]
    ├─┬ [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY @esbuild/[email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY @esbuild/[email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ ├── UNMET OPTIONAL DEPENDENCY [email protected]
    │ └── UNMET OPTIONAL DEPENDENCY [email protected]
    ├─┬ [email protected] invalid: "^0.6.20" from node_modules/@functionless/language-service
    │ ├── @aws-cdk/[email protected] deduped
    │ ├── @functionless/[email protected] deduped
    │ ├── @functionless/[email protected]
    │ ├── @swc/[email protected]
    │ ├── @swc/[email protected]
    │ ├── @swc/[email protected]
    │ ├── @types/[email protected]
    │ ├── [email protected] deduped
    │ ├── [email protected] deduped
    │ ├── [email protected] deduped
    │ ├── [email protected] deduped
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ ├── [email protected] deduped invalid: "^0.1.5" from node_modules/functionless
    │ └── [email protected] deduped
    ├─┬ [email protected]
    │ ├── @jest/[email protected]
    │ ├── @jest/[email protected]
    │ ├── [email protected]
    │ ├── [email protected]
    │ └── UNMET OPTIONAL DEPENDENCY node-notifier@^8.0.1 || ^9.0.0 || ^10.0.0
    ├─┬ [email protected] invalid: "^0.1.5" from node_modules/functionless
    │ ├── @aws-sdk/[email protected]
    │ ├── @aws-sdk/[email protected]
    │ ├── @aws-sdk/[email protected]
    │ ├── @aws-sdk/[email protected]
    │ ├── @aws-sdk/[email protected]
    │ ├── @types/[email protected] deduped
    │ └── [email protected] deduped
    └── [email protected]
    
    opened by cfraz89 0
  • chore: support ast-reflection s-expressions

    chore: support ast-reflection s-expressions

    Required for: https://github.com/functionless/ast-reflection/pull/32

    Maintains backwards compatibility with previous register and bind functions.

    AST-Reflection is changing from register and bind methods to using s-expression like sequence expressions.

    opened by thantos 1
  • Deprecate V1 closure serializer and default to V2

    Deprecate V1 closure serializer and default to V2

    With the release of https://github.com/functionless/functionless/pull/402, we now have two closures serializeres.

    • V1 - closure serializer based off of Pulumi's closures serializer. It uses the debugger API and is heavily limited, for example not properly serializing free variables such that mutations can be observed, cannot capture this, cannot serializer Proxies or bound functions, and has no source map support and therefore produces code that is not production worthy. The code for this serializer is also a spaghetti nightmare, making it impossible to maintain. Finally, the implementation is extremely SLOW and asynchronous, making it a bad fit for use within CDK Constructs.
    • V2 - our own serializer that takes advantage of the ast-reflection pre-processing step. We decorate all of a user's code with its AST information to support features such as IAM policy inference and the Functionless interpreteres. We now also use this information to provide a fast and synchronous closure serializer with out any of the aforementioned limitations. Our ast-reflection package can always be updated to inject more information to help support the serializer, meaning we are not constrained by the limitations of the V8 debugger API.

    As of now, we still default to the V1 serializer - users must opt-in to use V2 to get all of its benefits. This task is to go through the process of thoroughly testing the new serializer so that we are confident in moving over to it first class. V2 has been tested against all of the test cases in functionless but this does not give us the confidence we need.

    This task can be achieved int he following steps:

    • [ ] thoroughly explore test cases for the new serializer in serializer-cloures.test.ts - tests are pretty good but not thorough enough yet
    • [ ] update the default serializer to V2 but still allow users to opt-out and go back to V1
    • [ ] once we are confident enough, remove V1, delete the SerializerImpl enum, deprecate the @functionless/nodejs-closure-serializer package and delete the repository/change it to private.
    lambda 
    opened by sam-goodwin 1
Releases(v0.29.0)
Owner
sam
Generic and Functional programming. Let's-a-go!
sam
Grupprojekt för kurserna 'Javascript med Ramverk' och 'Agil Utveckling'

JavaScript-med-Ramverk-Laboration-3 Grupprojektet för kurserna Javascript med Ramverk och Agil Utveckling. Utvecklingsguide För information om hur utv

Svante Jonsson IT-Högskolan 3 May 18, 2022
Hemsida för personer i Sverige som kan och vill erbjuda boende till människor på flykt

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

null 4 May 3, 2022
Kurs-repo för kursen Webbserver och Databaser

Webbserver och databaser This repository is meant for CME students to access exercises and codealongs that happen throughout the course. I hope you wi

null 14 Jan 3, 2023
A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.

Features Web Bookmarks Service Bookmarks Docker Integration Status light + CPU, Memory & Network Reporting (click on the status light) Service Integra

Ben Phelps 3.5k Dec 30, 2022
Plugin that lets you create diagrams from textual representation (aka 'Diagrams as Code') within Logseq

Logseq - Diagrams as Code Plugin that lets you create diagrams (and other visualizations) from textual representation (aka 'Diagrams as Code') within

Nicolai P. Großer 80 Dec 21, 2022
Like Obsidian Publish but for self-hosting. Plugin integrations for dataview, admonition, and more.

Obsidian Export Obsidian Publish is great but lacks support for many of the plugins we Obsidian addicts have grown accustomed to — in particular Datav

null 12 Nov 28, 2022
aka Scaletor, take screenshots of a piece of a map and scale/compare with other parts of the map

scale-a-tron A quick-and-dirty map that lets you compare one area to another. Draw a shape around a region, zoom in to another place on the map, and c

Stamen Design 24 Nov 7, 2022
Hacking Instructions for the Nokia 800 Tough (aka Bananaphone with KaiOS)

Nokia 800 Tough Hacking Instructions for Arch Linux host system. I don't care about your messed up Windows, sorry. 1. Android Platform Tools Install t

Cookie Engineer 3 Jul 12, 2022
🎲 Extract one or more random elements from a weighted array (aka loot table or gacha)

wrand Extract one or more random elements from a weighted array. const items = [ { original: "Bronze", weight: 20 }, { original: "Silver", weight:

Leonardo Montini 14 Dec 2, 2022
a tweaked hackchat client. aka hackchat++.

hackchat-client-plus A tweaked hackchat client. aka hackchat++. Most code are from https://github.com/hack-chat/main. Hosted at https://hcer.netlify.a

null 7 Dec 24, 2022
An extension to download all you need in the LGU (aka CUHKSZ) Blackboard

LGU Blackboard Downloader A Chromium (Chrome/Edge compatible) browser extension to download all you need in the LGU (aka CUHKSZ) Blackboard 一个Chrome/E

Zcorn 4 Mar 4, 2023
A complete template for 2022 focused on around React, Postgres and various web3 integrations.

A complete template for 2022 focused on around React, Postgres and various web3 integrations. You can use the template to make a website, a web application, a hybrid decentralized web application, or even a DAO.

jim 45 Dec 22, 2022
API dot Open Sauced is NestJS and SupaBase powered OAS3 backend designed to remove client complexity and provide a structured graph of all @open-sauced integrations

?? Open Sauced Nest Supabase API ?? The path to your next Open Source contribution ?? Prerequisites In order to run the project we need the following

TED Vortex (Teodor-Eugen Duțulescu) 13 Dec 18, 2022
Open source infrastructure for scalable, reliable native integrations in B2B SaaS products

Open-source infrastructure for native integrations Native, customer-facing integrations for your B2B SaaS made simple, reliable and extensible. Explor

Nango 225 Jan 2, 2023
Windmill: Open-source platform and runtime to turn any scripts into internal apps, integrations and workflows

. Open-source and self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs to trigger flows and

Windmill Labs, Inc 1.6k Jan 4, 2023
Documentation: PWA integrations for Vite and the ecosystem

Documentation: PWA integrations for Vite and the ecosystem ?? Features ?? Documentation & guides ?? Zero-Config: sensible built-in default configs for

Vite PWA 17 Dec 28, 2022
Manage GitHub resources like repositories, teams, members, integrations and workflows with the AWS CDK as Custom Resources in CloudFormation.

CDK Github Manage GitHub resources like repositories, teams, members, integrations and workflows with the AWS CDK as Custom Resources in CloudFormatio

Pepperize 8 Nov 25, 2022
Service Installer for VMware Tanzu is a one-click automation solution that enables VMware field engineers to easily and rapidly install, configure, and operate VMware Tanzu services across a variety of cloud infrastructures.

Service Installer for VMware Tanzu Service Installer for VMware Tanzu seeks to provide a one-click automation solution to enable our VMware engineers

VMware Tanzu 42 Dec 1, 2022
An AWS Cloud Native application using CDK that defines a Serverless Event Driven application for interacting with Twitter and utilising Machine Learning / AI as a Service.

AWS Serverless Event Driven Twitter Bot An AWS Cloud Native application using CDK (Written in TypeScript) that defines a Serverless Event Driven appli

null 4 Dec 18, 2022