Today we released @aws-lite/client
0.6.0 and the first official service plugin (@aws-lite/dynamodb
). I selected Dynamo as a starting point because if its wide variety of methods, highly complex semantics (including using deeply nested AWS-flavored JSON), and ease of testing. This resulted in a bunch of changes to the plugin API thus far, including a new response()
lifecycle hook, passing some utilities, and a bunch of other changes and fixes.
Now, I'd like to say up front that the aws-lite
plugin API is still very much open to feedback – I am absolutely more than happy to reauthor @aws-lite/dynamodb
against any great suggestions that show up here.
With all that out of the way, let me copy/paste in the new plugin documentation and contributing guidelines now in the readme:
Plugins
Out of the box, @aws-lite/client
is a full-featured AWS API client that you can use to interact with any AWS service that makes use of authentication via AWS signature v4 (which should be just about all of them).
@aws-lite/client
can be extended with plugins to more easily interact with AWS services. A bit more about how plugins work:
- Plugins can be authored in ESM or CJS
- Plugins can be dependencies downloaded from npm, or also live locally in your codebase
- In conjunction with the open source community,
aws-lite
publishes service plugins under the @aws-lite/$service
namespace that conform to aws-lite
standards
@aws-lite/*
plugins, and packages published to npm with the aws-lite-plugin-*
prefix, are automatically loaded by the @aws-lite/client
upon instantiation
Thus, to make use of the @aws-lite/dynamodb
plugin, this is what your code would look like:
npm i @aws-lite/client @aws-lite/dynamodb
import awsLite from '@aws-lite/client'
const aws = await awsLite() // @aws-lite/dynamodb is now loaded
aws.dynamodb.PutItem({ TableName: 'my-table', Key: { id: 'hello' } })
Plugin API
The aws-lite
plugin API is lightweight and simple to learn. It makes use of four optional lifecycle hooks:
validate
[optional] - an object of property names and types to validate inputs with pre-request
request()
[optional] - an async function that enables mutation of inputs to the final service API request
response()
[optional] - an async function that enables mutation of service API responses before they are returned
error()
[optional] - an async function that enables mutation of service API errors before they are returned
The above four lifecycle hooks must be exported as an object named methods
, along with a valid AWS service code property named service
, like so:
// A simple plugin for validating input
export default {
service: 'dynamodb',
methods: {
PutItem: {
validate: {
TableName: { type: 'string', required: true }
}
}
}
}
// Using the above plugin
aws.dynamodb.PutItem({ TableName: 12345 }) // Throws validation error
Example plugins can be found below, in plugins/
dir (containing @aws-lite/*
plugins), and in tests.
validate
The validate
lifecycle hook is an optional object containing (case-sensitive) input property names, with a corresponding object that denotes their type
and whether required
.
Types are as follows: array
boolean
number
object
string
; additionally, a types
array may be supplied. An example validate
plugin:
// Validate inputs for a single DynamoDB method (`CreateTable`)
export default {
service: 'dynamodb',
methods: {
CreateTable: {
validate: {
TableName: { type: 'string', required: true },
AttributeDefinitions: { type: 'array', required: true },
KeySchema: { type: 'array', required: true },
BillingMode: { type: 'string' },
DeletionProtectionEnabled: { type: 'boolean' },
GlobalSecondaryIndexes: { type: 'array' },
LocalSecondaryIndexes: { type: 'array' },
ProvisionedThroughput: { type: 'object' },
SSESpecification: { type: 'object' },
StreamSpecification: { type: 'object' },
TableClass: { type: 'string' },
Tags: { type: 'array' },
}
}
}
}
request()
The request()
lifecycle hook is an optional async function that enables that enables mutation of inputs to the final service API request.
request()
is executed with two positional arguments:
params
(object)
- The method's input parameters
utils
(object)
- Helper utilities for (de)serializing AWS-flavored JSON (
awsjsonMarshall
, awsjsonUnmarshall
), config, creds, etc.
The request()
method may return nothing, or a valid client request. An example:
// Automatically serialize input to AWS-flavored JSON
export default {
service: 'dynamodb',
methods: {
PutItem: {
validate: { Item: { type: 'object', required: true } },
request: async (params, utils) => {
params.Item = utils.awsjsonMarshall(params.Item)
return {
headers: { 'X-Amz-Target': `DynamoDB_20120810.PutItem` }
payload: params
}
}
}
}
}
response()
The response()
lifecycle hook is an async function that enables mutation of service API responses before they are returned.
response()
is executed with two positional arguments:
response
(object)
- The
response
object contains the following properties:
headers
(object) - the raw response headers from the service API
statusCode
(number or undefined) - resulting status code of the API response; if an HTTP connection error occurred, no statusCode
will be present
payload
(any) - response payload
- If the entire
payload
is JSON, AWS-flavored JSON, or XML, aws-lite
will attempt to parse it prior to executing response()
. Responses that are primarily JSON, but with nested AWS-flavored JSON, will be parsed only as JSON and may require additional deserialization with the awsjsonUnmarshall
utility
utils
(object)
- Helper utilities for (de)serializing AWS-flavored JSON (
awsjsonMarshall
, awsjsonUnmarshall
), config, creds, etc.
The response()
method may return nothing, or anything (object, string, etc.). If returning an object, you may return the optional awsjson
property (that behaves the same as in client requests). An example:
// Automatically deserialize AWS-flavored JSON
export default {
service: 'dynamodb',
methods: {
GetItem: {
// Successful responses always have an AWS-flavored JSON `Item` property
response: async ({ payload }, utils) => {
return { awsjson: [ 'Item' ], payload }
}
}
}
}
error()
The error()
lifecycle hook is an async function that enables mutation of service API errors before they are returned.
error()
is executed with two positional arguments:
error
(object)
- The object containing the following properties:
error
(object or string) - the raw error from the service API
- If the entire
error
is JSON, AWS-flavored JSON, or XML, aws-lite
will attempt to parse it prior to executing response()
. Responses that are primarily JSON, but with nested AWS-flavored JSON, will be parsed only as JSON and may require additional deserialization with the awsjsonUnmarshall
utility
headers
(object) - the raw response headers from the service API
metadata
(object) - aws-lite
error metadata; to improve the quality of the errors presented by aws-lite
, please only append to this object
statusCode
(number or undefined) - resulting status code of the API response; if an HTTP connection error occurred, no statusCode
will be present
utils
(object)
- Helper utilities for (de)serializing AWS-flavored JSON (
awsjsonMarshall
, awsjsonUnmarshall
), config, creds, etc.
The error()
method may return nothing, a new or mutated version of the error payload it was passed, a string, an object, or a JS error. An example
// Improve clarity of error output
export default {
service: 'lambda',
methods: {
GetFunctionConfiguration: {
error: async (err, utils) => {
if (err.statusCode === 400 &&
err?.error?.message?.match(/validation/)) {
// Append a property to be clearly displayed along with the other error data
err.metadata.type = 'Validation error'
}
return err
}
}
}
}
Authoring @aws-lite/*
plugins
Similar to the Definitely Typed (@types
) model, aws-lite
releases packages maintained by third parties under the @aws-lite/*
namespace.
Plugins released within the @aws-lite/*
namespace are expected to conform to the following standards:
@aws-lite/*
plugins should read more or less like the others, and broadly adhere to the following style:
- Plugins should be authored in ESM, be functional (read: no classes), and avoid globals / closures, etc. wherever possible
- Plugins should be authored in JavaScript; those that require transpilation (e.g. TypeScript) will not be accepted
- Plugins should cover all documented methods for a given service, and include links for each method within the plugin
- Each plugin is singular for a given service
- Example: we will not ship
@aws-lite/lambda
, @aws-lite/lambda-1
, @aws-lite/lambda-new
, etc.
- With permission of the current maintainer(s), you may become a maintainer of an existing plugin
- To maintain the speed, security, and lightweight size of the
aws-lite
project, plugins should ideally have zero external dependencies
- If external dependencies are absolutely necessary, they should be justifiable; expect their inclusion to be heavily audited
- Ideally (but not necessarily), each plugin should include its own tests
- Tests should follow the project's testing methodology, utilizing
tape
as the runner and tap-arc
as the output parser
- Tests should not rely on interfacing with live AWS services
- Wherever possible, plugin maintainers should attempt to employ manual verification of their plugins during development
- By opting to author a plugin, you are opting to provide reasonably prompt bug fixes, updates, etc. for the community
- If you are not willing to make that kind of commitment but still want to publish your plugins publicly, please feel free to do so outside this repo with an
aws-lite-plugin-
package prefix