Skip to content

Commit

Permalink
add basic function implementation (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
edwardfoyle authored Jul 24, 2023
1 parent 9016863 commit b5afd08
Show file tree
Hide file tree
Showing 40 changed files with 2,436 additions and 66 deletions.
41 changes: 38 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions packages/backend-function/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Be very careful editing this file. It is crafted to work around [this issue](https://github.com/npm/npm/issues/4479)

# First ignore everything
**/*

# Then add back in transpiled js and ts declaration files
!lib/**/*.js
!lib/**/*.d.ts

# Then ignore test js and ts declaration files
*.test.js
*.test.d.ts

# This leaves us with including only js and ts declaration files of functional code
40 changes: 40 additions & 0 deletions packages/backend-function/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## API Report File for "@aws-amplify/backend-function"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import { AmplifyFunction } from '@aws-amplify/function-construct';
import { AmplifyFunctionProps } from '@aws-amplify/function-construct';
import { ConstructFactory } from '@aws-amplify/plugin-types';
import { ConstructFactoryGetInstanceProps } from '@aws-amplify/plugin-types';

// @public
export class AmplifyFunctionFactory implements ConstructFactory<AmplifyFunction> {
static build(props: AmplifyFunctionFactoryBuildProps): Promise<AmplifyFunctionFactory>;
static fromDir(props: AmplifyFunctionFactoryFromDirProps): AmplifyFunctionFactory;
getInstance({ constructContainer, }: ConstructFactoryGetInstanceProps): AmplifyFunction;
}

// @public (undocumented)
export type AmplifyFunctionFactoryBaseProps = {
name: string;
};

// @public (undocumented)
export type AmplifyFunctionFactoryBuildProps = AmplifyFunctionFactoryBaseProps & Omit<AmplifyFunctionProps, 'absoluteCodePath'> & {
buildCommand: string;
outDir: string;
};

// @public (undocumented)
export type AmplifyFunctionFactoryFromDirProps = AmplifyFunctionFactoryBaseProps & Omit<AmplifyFunctionProps, 'absoluteCodePath'> & {
codePath: string;
};

// @public
export const Func: typeof AmplifyFunctionFactory;

// (No @packageDocumentation comment for this package)

```
3 changes: 3 additions & 0 deletions packages/backend-function/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../api-extractor.base.json"
}
28 changes: 28 additions & 0 deletions packages/backend-function/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@aws-amplify/backend-function",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js",
"require": "./lib/index.js"
}
},
"types": "lib/index.d.ts",
"scripts": {
"update:api": "api-extractor run --local"
},
"dependencies": {
"@aws-amplify/function-construct": "^0.1.0",
"execa": "^7.1.1"
},
"devDependencies": {
"@aws-amplify/backend-engine": "^0.1.0",
"@aws-amplify/plugin-types": "^0.1.0"
},
"peerDependencies": {
"aws-cdk-lib": "~2.68.0",
"constructs": "^10.0.0"
}
}
74 changes: 74 additions & 0 deletions packages/backend-function/src/factory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { beforeEach, describe, it, mock } from 'node:test';
import { Func } from './factory.js';
import { App, Stack } from 'aws-cdk-lib';
import {
NestedStackResolver,
SingletonConstructContainer,
StackMetadataBackendOutputStorageStrategy,
} from '@aws-amplify/backend-engine';
import {
BackendOutputEntry,
BackendOutputStorageStrategy,
ConstructContainer,
} from '@aws-amplify/plugin-types';
import assert from 'node:assert';
import { fileURLToPath } from 'url';

describe('AmplifyFunctionFactory', () => {
let constructContainer: ConstructContainer;
let outputStorageStrategy: BackendOutputStorageStrategy<BackendOutputEntry>;

beforeEach(() => {
const app = new App();
const stack = new Stack(app, 'testStack');

constructContainer = new SingletonConstructContainer(
new NestedStackResolver(stack)
);

outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy(
stack
);
});

it('creates singleton function instance', () => {
const functionFactory = Func.fromDir({
name: 'testFunc',
codePath: '../test-assets/test-lambda',
});
const instance1 = functionFactory.getInstance({
constructContainer,
outputStorageStrategy,
});
const instance2 = functionFactory.getInstance({
constructContainer,
outputStorageStrategy,
});
assert.strictEqual(instance1, instance2);
});

it('executes build command from directory where constructor is used', async () => {
const commandExecutorMock = mock.fn();

// Casting to never is necessary because commandExecutor is a private method.
// TS yells that it's not a property on Func even though it is there
mock.method(Func, 'commandExecutor' as never, commandExecutorMock);

(
await Func.build({
name: 'testFunc',
outDir: '../test-assets/test-lambda',
buildCommand: 'test command',
})
).getInstance({ constructContainer, outputStorageStrategy });

assert.strictEqual(commandExecutorMock.mock.callCount(), 1);
assert.deepStrictEqual(commandExecutorMock.mock.calls[0].arguments, [
'test command',
{
cwd: fileURLToPath(new URL('../src', import.meta.url)),
stdio: 'inherit',
},
]);
});
});
135 changes: 135 additions & 0 deletions packages/backend-function/src/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
ConstructContainerEntryGenerator,
ConstructFactory,
ConstructFactoryGetInstanceProps,
} from '@aws-amplify/plugin-types';
import {
AmplifyFunction,
AmplifyFunctionProps,
} from '@aws-amplify/function-construct';
import { Construct } from 'constructs';
import { execaCommand } from 'execa';
import * as path from 'path';
import { getCallerDirectory } from './get_caller_directory.js';

export type AmplifyFunctionFactoryBaseProps = {
/**
* A name for the function that is used to disambiguate it from other functions in the project
*/
name: string;
};

export type AmplifyFunctionFactoryBuildProps = AmplifyFunctionFactoryBaseProps &
Omit<AmplifyFunctionProps, 'absoluteCodePath'> & {
/**
* The command to run that generates built function code.
* This command is run from the directory where this factory is called
*/
buildCommand: string;
/**
* The buildCommand is expected to place build artifacts at this location.
* This path can be relative or absolute. If relative, the absolute path is calculated based on the directory where this factory is called
*/
outDir: string;
};

export type AmplifyFunctionFactoryFromDirProps =
AmplifyFunctionFactoryBaseProps &
Omit<AmplifyFunctionProps, 'absoluteCodePath'> & {
/**
* The location of the pre-built function code.
* Can be a directory or a .zip file.
* Can be a relative or absolute path. If relative, the absolute path is calculated based on the directory where this factory is called.
*/
codePath: string;
};

type AmplifyFunctionFactoryProps = AmplifyFunctionFactoryBaseProps &
AmplifyFunctionProps;

/**
* Create Lambda functions in the context of an Amplify backend definition
*/
export class AmplifyFunctionFactory
implements ConstructFactory<AmplifyFunction>
{
// execaCommand is assigned to a static prop so that it can be mocked in tests
private static commandExecutor = execaCommand;

private generator: ConstructContainerEntryGenerator;
/**
* Create a new AmplifyFunctionFactory
*/
private constructor(private readonly props: AmplifyFunctionFactoryProps) {}

/**
* Create a function from a directory that contains pre-built code
*/
static fromDir(
props: AmplifyFunctionFactoryFromDirProps
): AmplifyFunctionFactory {
const absoluteCodePath = path.isAbsolute(props.codePath)
? props.codePath
: path.resolve(getCallerDirectory(new Error().stack), props.codePath);
return new AmplifyFunctionFactory({
name: props.name,
absoluteCodePath,
runtime: props.runtime,
handler: props.handler,
});
}

/**
* Create a function by executing a build command that places build artifacts at a specified location
*
* TODO: Investigate long-term function building strategy: https://github.com/aws-amplify/samsara-cli/issues/92
*/
static async build(
props: AmplifyFunctionFactoryBuildProps
): Promise<AmplifyFunctionFactory> {
const importPath = getCallerDirectory(new Error().stack);

await AmplifyFunctionFactory.commandExecutor(props.buildCommand, {
cwd: importPath,
stdio: 'inherit',
});

const absoluteCodePath = path.isAbsolute(props.outDir)
? props.outDir
: path.resolve(importPath, props.outDir);

return new AmplifyFunctionFactory({
name: props.name,
absoluteCodePath,
runtime: props.runtime,
handler: props.handler,
});
}

/**
* Creates an instance of AmplifyFunction within the provided Amplify context
*/
getInstance({
constructContainer,
}: ConstructFactoryGetInstanceProps): AmplifyFunction {
if (!this.generator) {
this.generator = new AmplifyFunctionGenerator(this.props);
}
return constructContainer.getOrCompute(this.generator) as AmplifyFunction;
}
}

class AmplifyFunctionGenerator implements ConstructContainerEntryGenerator {
readonly resourceGroupName = 'function';

constructor(private readonly props: AmplifyFunctionFactoryProps) {}

generateContainerEntry(scope: Construct) {
return new AmplifyFunction(scope, this.props.name, this.props);
}
}

/**
* Alias for AmplifyFunctionFactory
*/
export const Func = AmplifyFunctionFactory;
Loading

0 comments on commit b5afd08

Please sign in to comment.