diff --git a/packages/sls-aws/package-lock.json b/packages/sls-aws/package-lock.json new file mode 100644 index 0000000..8ca6f19 --- /dev/null +++ b/packages/sls-aws/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "@cabiri-io/sls-aws", + "version": "0.0.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@cabiri-io/sls-env": { + "version": "0.0.2", + "resolved": "https://npm.pkg.github.com/download/@cabiri-io/sls-env/0.0.2/d10d7fd76a625b8d2c3d548bdfd476b0c96c9093940281cf072e21a0c2da70d7", + "integrity": "sha512-1QMfrf2eTZO0nVcVBR87nOqzABrEvEM7HFWdw3YSHX8MxYzUcS6SP9jfzYLLmvjZcTuiMNHLWhal3U/wREV63Q==", + "requires": { + "lodash.camelcase": "^4.3.0" + } + }, + "@types/aws-lambda": { + "version": "8.10.64", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.64.tgz", + "integrity": "sha512-LRKk2UQCSi7BsO5TlfSI8cTNpOGz+MH6+RXEWtuZmxJficQgxwEYJDiKVirzgyiHce0L0F4CqCVvKTwblAeOUw==", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + } + } +} diff --git a/packages/sls-aws/src/api-gateway/index.ts b/packages/sls-aws/src/api-gateway/index.ts new file mode 100644 index 0000000..9e6a265 --- /dev/null +++ b/packages/sls-aws/src/api-gateway/index.ts @@ -0,0 +1,9 @@ +import { EnvConfig, Handler, SlsEnvironment, environment } from '@cabiri-io/sls-env' +// eslint-disable-next-line import/no-unresolved +import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2, Context } from 'aws-lambda' + +type APIGatewayV2Handler = Handler> + +export const apiGatewayV2 = ( + config?: EnvConfig +): SlsEnvironment, C, D, P, R> => environment, C, D, P, R>(config) diff --git a/packages/sls-aws/src/index.ts b/packages/sls-aws/src/index.ts index 47d93a0..e89a352 100644 --- a/packages/sls-aws/src/index.ts +++ b/packages/sls-aws/src/index.ts @@ -1,11 +1,2 @@ -import { EnvConfig, SlsEnvironment, environment } from '@cabiri-io/sls-env' -// eslint-disable-next-line import/no-unresolved -import type { Context, SNSEvent } from 'aws-lambda' -import { jsonSNSMessage } from './sns/json-sns-message' -import { jsonSNSMessages } from './sns/json-sns-messages' - -export const snsMessage = (config?: EnvConfig): SlsEnvironment => - environment(config).payload(jsonSNSMessage) - -export const snsMessages = (config?: EnvConfig): SlsEnvironment, void> => - environment, void>(config).payload(jsonSNSMessages) +export { snsMessage, snsMessages } from './sns' +export { apiGatewayV2 } from './api-gateway' diff --git a/packages/sls-aws/src/sns/index.ts b/packages/sls-aws/src/sns/index.ts new file mode 100644 index 0000000..5d9a6a8 --- /dev/null +++ b/packages/sls-aws/src/sns/index.ts @@ -0,0 +1,13 @@ +import { EnvConfig, Handler, SlsEnvironment, environment } from '@cabiri-io/sls-env' +// eslint-disable-next-line import/no-unresolved +import type { Context, SNSEvent } from 'aws-lambda' +import { jsonSNSMessage } from './json-sns-message' +import { jsonSNSMessages } from './json-sns-messages' + +type SNSHandler = Handler + +export const snsMessage = (config?: EnvConfig): SlsEnvironment => + environment(config).payload(jsonSNSMessage) + +export const snsMessages = (config?: EnvConfig): SlsEnvironment> => + environment>(config).payload(jsonSNSMessages) diff --git a/packages/sls-env/package-lock.json b/packages/sls-env/package-lock.json new file mode 100644 index 0000000..0264568 --- /dev/null +++ b/packages/sls-env/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "@cabiri-io/sls-env", + "version": "0.0.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/lodash": { + "version": "4.14.165", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz", + "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==", + "dev": true + }, + "@types/lodash.camelcase": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/lodash.camelcase/-/lodash.camelcase-4.3.6.tgz", + "integrity": "sha512-hd/TEuPd76Jtf1xEq85CHbCqR+iqvs5IOKyrYbiaOg69BRQgPN9XkvLj8Jl8rBp/dfJ2wQ1AVcP8mZmybq7kIg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.snakecase": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/lodash.snakecase/-/lodash.snakecase-4.1.6.tgz", + "integrity": "sha512-qGTf27ncTRUhSwvxD0hzYFmelmTrzEBGvBigrLyx6PRN1rKuy0ZEK+A3X3QnW7k+CwEjIJeAM6XBN4Ay6F03IQ==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "dev": true + } + } +} diff --git a/packages/sls-env/src/__tests__/index.test.ts b/packages/sls-env/src/__tests__/index.test.ts index 4ee4c3a..2f9e249 100644 --- a/packages/sls-env/src/__tests__/index.test.ts +++ b/packages/sls-env/src/__tests__/index.test.ts @@ -1,14 +1,14 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/ban-types */ import snakeCase from 'lodash.snakecase' -import { Logger, environment } from '..' +import { Handler, Logger, environment } from '..' type EmptyEvent = {} type EmptyContext = {} describe('serverless environment', () => { it('supports application', async () => - environment() + environment, never, void, void>() .app(() => 'hello world!') .start({}, {}) .then(result => expect(result).toBe('hello world!'))) @@ -18,7 +18,7 @@ describe('serverless environment', () => { type EventPayload = { event: MessageEvent; context: NameContext } it('supports passing an event and context to application', async () => - environment() + environment, never, void, EventPayload>() .app(({ payload: { event, context } }) => `${event.message} ${context.name}!`) .start({ message: 'hello' }, { name: 'world' }) .then(result => expect(result).toBe('hello world!'))) @@ -32,7 +32,7 @@ describe('serverless environment', () => { const buildMessage: BuildMessage = (message, name) => `${message} ${name}!` - return environment() + return environment, never, BuildMessageDependencies, EventPayload>() .global(buildMessage) .app(({ payload: { event, context }, dependencies: { buildMessage } }) => buildMessage(event.message, context.name) @@ -47,7 +47,7 @@ describe('serverless environment', () => { buildMessage: BuildMessage } - return environment() + return environment, never, BuildMessageDependencies, EventPayload>() .global({ buildMessage: (message: string, name: string) => `${message} ${name}!` }) .app(({ payload: { event, context }, dependencies: { buildMessage } }) => buildMessage(event.message, context.name) @@ -62,7 +62,7 @@ describe('serverless environment', () => { buildMessage: BuildMessage } - return environment() + return environment, never, BuildMessageDependencies, EventPayload>() .global('buildMessage', (message: string, name: string) => `${message} ${name}!`) .app(({ payload: { event, context }, dependencies: { buildMessage } }) => buildMessage(event.message, context.name) @@ -72,6 +72,11 @@ describe('serverless environment', () => { }) }) + // type A = (a: AA) => AB + // type C> = (a: AC) => AD + + // const a: C> = a => b => a + b + describe('payload', () => { it('supports mapping payload to custom type', async () => { type HelloWorldMessage = { @@ -87,7 +92,12 @@ describe('serverless environment', () => { buildMessage: BuildHelloWorldMessage } - return environment() + return environment< + Handler, + never, + BuildHelloWorldMessageDependencies, + HelloWorldMessage + >() .global(buildMessage) .payload((event, context) => ({ hello: event.message, @@ -100,7 +110,7 @@ describe('serverless environment', () => { it('fails when added multiple times', () => expect(() => - environment() + environment, never, void, void>() .app(() => 'hello world!') .payload(() => {}) .payload(() => {}) @@ -109,7 +119,7 @@ describe('serverless environment', () => { describe('logger', () => { it('has default built in', () => - environment() + environment, never, void, void>() .app(({ logger }) => expect(logger).toBeDefined()) .start({}, {})) @@ -121,7 +131,7 @@ describe('serverless environment', () => { } } as unknown) as Logger - return environment() + return environment, never, void, void>() .logger(logger) .app(({ logger }) => logger.debug('log message')) .start({}, {}) @@ -131,6 +141,7 @@ describe('serverless environment', () => { }) }) + // maybe we should have environment variables and all should be consider app configuration? describe('environment variables mapper', () => { // how do we define environment variables it('default to camel case', () => { @@ -138,7 +149,7 @@ describe('serverless environment', () => { process.env.WORKSPACE_NAME = 'workspaceNameValue' type EnvDependency = { env: { workspace: string; workspaceName: string } } - return environment() + return environment, never, EnvDependency, void>() .app(({ dependencies: { env } }) => { expect(env.workspace).toBe('workspaceValue') expect(env.workspaceName).toBe('workspaceNameValue') @@ -147,11 +158,12 @@ describe('serverless environment', () => { }) it('allows to override default variable name', () => { + // fixme: all the env needs to be pushed to configuration and be described as types process.env.WORKSPACE1 = 'workspace1Value' process.env.WORKSPACE1_NAME = 'workspaceName1Value' type EnvDependency = { env: { workspace_1: string; workspace_1_name: string } } - return environment({ + return environment, never, EnvDependency, void>({ envNameMapper: snakeCase }) .app(({ dependencies: { env } }) => { @@ -161,14 +173,39 @@ describe('serverless environment', () => { .start({}, {}) }) }) - // google run against express - // it('runs request / response environment', () => environment().run(request, response)) - // it('runs google function environment', () => environment().run(event, context)) - /* - const ecbMapping = { - environment: 'ENVIRONMENT', - serviceName: 'SERVICE_NAME', - workspace: 'WORKSPACE', -} - */ + + describe('application configuration', () => { + // should app configuration be passed to dependecies + // that could be good as then we can use to to configure all the clients and their timeouts + // + it('creates config from pure function', () => { + type AppConfig = { + message: string + } + + return environment, AppConfig, never, void>() + .config(() => ({ message: 'hello' })) + .app(({ config }) => { + expect(config.message).toBe('hello') + }) + .start({}, {}) + }) + + it('creates config from function returning promise', () => { + type AppConfig = { + message: string + } + + return environment, AppConfig, never, void>() + .config(async () => ({ message: 'hello' })) + .app(({ config }) => { + expect(config.message).toBe('hello') + }) + .start({}, {}) + }) + + it.todo('create config from object factory') + }) + + // where and how are we going to add correllaction id }) diff --git a/packages/sls-env/src/index.ts b/packages/sls-env/src/index.ts index 2ceea24..33a1e63 100644 --- a/packages/sls-env/src/index.ts +++ b/packages/sls-env/src/index.ts @@ -4,7 +4,6 @@ import camelCase from 'lodash.camelcase' import { PayloadDefinitionError } from './error/payload-definition-error' -type PayloadConstructor = (event: E, context: C) => R // but that makes it very specific to even and context // so maybe we extract Request to be Request {event, context} // or maybe request becomes something abstract @@ -20,12 +19,14 @@ type Event = { // event = where Event can be Request, // event = {input, context?, output?} // or maybe at this point we don't allow working with raw events as a good practice and abstraction -type AppConstructor = ({ +type AppConstructor = ({ payload, dependencies, + config, logger }: { payload: P + config: C dependencies: D logger: Logger }) => R | Promise @@ -56,23 +57,47 @@ export type Logger = { trace: LogFn } -// E - event -// C - context +export type Handler = (event: E, context: C) => R + +type PayloadConstructor, R> = (event: Parameters[0], context: Parameters[1]) => R + +type SuccessHandler = (i: I) => O + +// EventType & ContextType => LambdaResult - that needs to be just a handler // D - dependencies +// C - config +// P - payload // R - result -export type SlsEnvironment = { +// Handler +export type SlsEnvironment< + H extends Handler, + ConfigType, + DependentyType, + PayloadType, + R = ReturnType +> = { // but that may not be true as result should be handled by response // .errorResponseHandler(apiGatewayHandler, errorMapper) - errorHandler: () => SlsEnvironment + errorHandler: () => SlsEnvironment // .successResponseHandler(apiGatewayHandler, successResponseMapper) - successHandler: () => SlsEnvironment + successHandler: ( + handler: SuccessHandler> + ) => SlsEnvironment global: ( - ...func: [Function] | [ObjectUnionDependencies] | TupleUnionDependencies - ) => SlsEnvironment - logger: (logger: Logger) => SlsEnvironment - payload: (payloadConstructor: PayloadConstructor) => SlsEnvironment - app: (app: AppConstructor) => SlsEnvironment - start: (event: E, context: C) => Promise + ...func: [Function] | [ObjectUnionDependencies] | TupleUnionDependencies + ) => SlsEnvironment + logger: (logger: Logger) => SlsEnvironment + config: ( + config: () => ConfigType | Promise + ) => SlsEnvironment + payload: ( + payloadConstructor: PayloadConstructor + ) => SlsEnvironment + app: ( + app: AppConstructor + ) => SlsEnvironment + start: (...params: Parameters) => Promise> // actually that is not true the return value will be different as that will be tight to Lambda Handler + // for example in context of ApiGateway that will be {body: ..., statusCode: ...} } export type EnvConfig = { @@ -84,8 +109,22 @@ const passThroughPayloadMapping = (event: E, context: C) => (({ event, // with this level abstraction we would need to have awsEnv // with this level abstraction we would need to have expressEnv // with this level abstraction we would need to have googleFunctionEnv -export const environment = (_config?: EnvConfig): SlsEnvironment => { - let appConstructor: AppConstructor +// + +// if we just use env that does not have any typing which makes solution not type safe +// I think it make more sense to move just to config and copy any envs to config when required + +// each app has to have a config +export const environment = < + H extends Handler, + ConfigType, + DependencyType, + PayloadType, + R = ReturnType +>( + _config?: EnvConfig +): SlsEnvironment => { + let appConstructor: AppConstructor // todo: it would be probably better if that is typed as the rest of the framework // can we pick only the one that are there not go through x number of env variables const envNameMapper = _config?.envNameMapper ?? camelCase @@ -96,10 +135,12 @@ export const environment = (_config?: EnvConfig): SlsEnvironment< // at the moment we are cheating // promise of dependencies // how do we define that envs are always there in typescript for dependencies - let dependencies: D = ({ env } as unknown) as D + let dependencies: DependencyType = ({ env } as unknown) as DependencyType // todo: we need to have something like no payload - let payloadFactory: PayloadConstructor = passThroughPayloadMapping + let payloadFactory: PayloadConstructor = passThroughPayloadMapping let logger: Logger = console + let config: Promise = Promise.resolve({} as unknown) as Promise + let successHandler: SuccessHandler> = i => (i as unknown) as ReturnType return { errorHandler() { return this @@ -107,6 +148,15 @@ export const environment = (_config?: EnvConfig): SlsEnvironment< successHandler() { return this }, + config(appConfigConstructor) { + config = Promise.resolve(appConfigConstructor()) + return this + }, + // maybe it will be just easier to configure that through + // global({ + // module1: module1, + // module2: module2, + // }) why would you want to create multiple globals? I think that needs to be simpler // dependency // or maybe we have something similar to app and we have a function that // creates dependencies @@ -116,6 +166,7 @@ export const environment = (_config?: EnvConfig): SlsEnvironment< // dependency name string, and function // function with name // pass a promise and say that will resolve finally to your dependency + // maybe this is just an object with all the values instead of having all this global config global(...dependency) { // todo: how about dependencies being wrapped in promises we would need to unwrap them so we can store things like SSM result in there // todo: disable adding env to dependencies @@ -155,12 +206,15 @@ export const environment = (_config?: EnvConfig): SlsEnvironment< // now you can really chain that nicely // maybe we start currying // Promise.resolve().then(appConstructor(event, context)) - Promise.resolve() - .then(() => ({ + Promise.resolve(config) + // here we need to add dependencies as well + .then(config => ({ payload: payloadFactory(event, context), dependencies, + config, logger })) .then(appConstructor) + .then(successHandler) } }