-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from cabiri-io/feat/app-idea
feat: all in shutters but starts introducing generics
- Loading branch information
Showing
16 changed files
with
5,062 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"singleQuote": true, | ||
"printWidth": 120, | ||
"semi": false, | ||
"trailingComma": "none", | ||
"arrowParens": "avoid" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"typescript.tsdk": "node_modules/typescript/lib" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"engines": { | ||
"node": "0.12" | ||
}, | ||
"private": true, | ||
"scripts": { | ||
"commit": "git-cz", | ||
"pretty-quick": "pretty-quick", | ||
"prettify:fix": "prettier --write 'packages/**/*.{js,ts}'", | ||
"prettify": "prettier --check 'packages/**/*.{js,ts}'" | ||
}, | ||
"workspaces": [ | ||
"packages/*" | ||
], | ||
"devDependencies": { | ||
"@babel/core": "^7.9.6", | ||
"@babel/preset-env": "^7.9.6", | ||
"@babel/preset-typescript": "^7.9.0", | ||
"babel-jest": "^26.0.1", | ||
"jest": "^26.0.1", | ||
"prettier": "^2.0.5", | ||
"pretty-quick": "^2.0.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
setupFilesAfterEnv: ['jest-extended'] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "@cabiri-io/sls-app", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"test": "jest" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^26.0.0", | ||
"jest": "^26.0.1", | ||
"jest-extended": "^0.11.5", | ||
"ts-jest": "^26.1.0", | ||
"typescript": "^3.9.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
type ActionFunction<P, D, R> = (payload: P, dependencies: D) => R | ||
|
||
// configuration I want to be able to debug invocation | ||
type Application<P, D, R> = { | ||
pre(actionContextFunction: ActionContextFunction<P, D>): Application<P, D, R> | ||
action(actionFunction: ActionFunction<P, D, R>): Application<P, D, R> | never | ||
post(): Application<P, D, R> | ||
run(payload: P, dependencies: D): Promise<R> | ||
} | ||
|
||
type PreActionContext<P, D> = { | ||
payload: P | ||
dependencies: D | ||
} | ||
|
||
type ActionContextFunction<P, D> = (actionContext: PreActionContext<P, D>) => void | ||
|
||
type PreAction<P, D> = { | ||
func: ActionContextFunction<P, D> | ||
// maybe we have type | ||
} | ||
|
||
export class ApplicationError extends Error { | ||
public type: string = 'ApplicationError' | ||
} | ||
|
||
// allow to configure the application e.g. log each entry | ||
export function application<P, D, R>(): Application<P, D, R> { | ||
// maybe instead of using array we an explore using Task concept and | ||
// and instead we can just compose function | ||
|
||
const preActions: Array<PreAction<P, D>> = [] | ||
let mainAction: ActionFunction<P, D, R> | ||
return { | ||
pre(actionFunction) { | ||
// allow to push logger | ||
preActions.push({ func: actionFunction }) | ||
return this | ||
}, | ||
|
||
action(actionFunction) { | ||
//@ts-ignore | ||
if (mainAction) throw new ApplicationError('you can only have a single action') | ||
mainAction = actionFunction | ||
return this | ||
}, | ||
|
||
post() { | ||
return this | ||
}, | ||
|
||
run(payload, dependencies) { | ||
return ( | ||
preActions | ||
// we work with promises because this allows us easy capture all the async stuff | ||
.reduce((acc, v) => acc.then(r => (v.func(r), r)), Promise.resolve({ payload, dependencies })) | ||
// do action now will work with pre | ||
.then(({ payload, dependencies }) => mainAction(payload, dependencies)) | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { application, ApplicationError } from '../src' | ||
|
||
describe('application flow', () => { | ||
describe('action', () => { | ||
it('creates an application with an action only', () => | ||
application<string, { appendString: string }, string>() | ||
.action((payload, { appendString }) => `${payload} ${appendString}`) | ||
.run('hello', { appendString: 'world' }) | ||
.then(result => { | ||
expect(result).toBe('hello world') | ||
})) | ||
|
||
it('throws an error when multiple actions are created', () => | ||
expect(() => | ||
application<string, { appendString: string }, string>() | ||
.action((payload, { appendString }) => `${payload} ${appendString}`) | ||
.action((payload, { appendString }) => `${payload} ${appendString}`) | ||
.run('hello', { appendString: 'world' }) | ||
).toThrowError(new ApplicationError('you can only have a single action'))) | ||
}) | ||
|
||
describe('pre action', () => { | ||
it('execute pre action before main action', async () => { | ||
const preAction = jest.fn() | ||
const action = jest.fn() | ||
|
||
return application<string, { appendString: string }, void>() | ||
.pre(preAction) | ||
.action(action) | ||
.run('hello', { appendString: 'world' }) | ||
.then(() => { | ||
expect(preAction).toHaveBeenCalledWith({ payload: 'hello', dependencies: { appendString: 'world' } }) | ||
expect(action).toHaveBeenCalledWith('hello', { appendString: 'world' }) | ||
//@ts-ignore | ||
expect(preAction).toHaveBeenCalledBefore(action) | ||
}) | ||
}) | ||
|
||
it('executes pre actions in order of definition', async () => { | ||
const preAction1 = jest.fn() | ||
const preAction2 = jest.fn() | ||
const action = jest.fn() | ||
|
||
return application<string, { appendString: string }, void>() | ||
.pre(preAction1) | ||
.pre(preAction2) | ||
.action(action) | ||
.run('hello', { appendString: 'world' }) | ||
.then(() => { | ||
expect(preAction1).toHaveBeenCalledWith({ payload: 'hello', dependencies: { appendString: 'world' } }) | ||
expect(preAction2).toHaveBeenCalledWith({ payload: 'hello', dependencies: { appendString: 'world' } }) | ||
expect(action).toHaveBeenCalledWith('hello', { appendString: 'world' }) | ||
//@ts-ignore | ||
expect(preAction1).toHaveBeenCalledBefore(preAction2) | ||
//@ts-ignore | ||
expect(preAction2).toHaveBeenCalledBefore(action) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "./dist", | ||
"declaration": true | ||
}, | ||
"include": ["src/**/*", "tests/**/*"], | ||
"exclude": ["dist/**/*"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
setupFilesAfterEnv: ['jest-extended'] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "@cabiri-io/sls-env", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"test": "jest" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^26.0.0", | ||
"jest": "^26.0.1", | ||
"jest-extended": "^0.11.5", | ||
"ts-jest": "^26.1.0", | ||
"typescript": "^3.9.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// | ||
|
||
type PayloadConstructor<E, C, R> = (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 | ||
// what does Request mean in context of async event | ||
type Event<I, C, O> = { | ||
input: I | ||
context?: C | ||
output?: O | ||
} | ||
|
||
// maybe application gets event, context under something different like `dependencies` | ||
// event = <Event, Context, Response> 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<P, D, R> = ({ payload, dependencies }: { payload: P; dependencies: D }) => R | Promise<R> | ||
|
||
// E - event | ||
// C - context | ||
// D - dependencies | ||
// R - result | ||
type SlsEnvironment<E, C, D, P, R> = { | ||
// but that may not be true as result should be handled by response | ||
global: (func: Function) => SlsEnvironment<E, C, D, P, R> | ||
payload: (payloadConstructor: PayloadConstructor<E, C, P>) => SlsEnvironment<E, C, D, P, R> | ||
app: (app: AppConstructor<P, D, R>) => SlsEnvironment<E, C, D, P, R> | ||
start: (event: E, context: C) => Promise<R> | ||
} | ||
|
||
type SlsEnvironmentConfig = {} | ||
|
||
// maybe we have really generic environment and then we have wrappers for all different scenarios? | ||
// 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 = <E, C, D, P, R>(config?: SlsEnvironmentConfig): SlsEnvironment<E, C, D, P, R> => { | ||
let appConstructor: AppConstructor<P, D, R> | ||
// at the moment we are cheating | ||
let dependencies: D = ({} as unknown) as D | ||
// todo: we need to have something like no payload | ||
let payloadFactory: PayloadConstructor<E, C, P> = (event, context) => (({ event, context } as unknown) as P) | ||
return { | ||
// dependency | ||
// or maybe we have something similar to app and we have a function that | ||
// creates dependencies | ||
// dependencies(deps().global().global().prototype().create) | ||
// dependencies(deps().global().global().prototype()) | ||
global(func: Function) { | ||
dependencies = { ...dependencies, [func.name]: func } | ||
return this | ||
}, | ||
payload(payloadConstructor) { | ||
payloadFactory = payloadConstructor | ||
return this | ||
}, | ||
app(appFactory) { | ||
// maybe we wrap that with dependencies | ||
// ({}) => appFactory | ||
appConstructor = appFactory | ||
// how can we remove usage of this | ||
return this | ||
}, | ||
start: async (event, context) => { | ||
// now you can really chain that nicely | ||
// maybe we start currying | ||
// | ||
// Promise.resolve().then(appConstructor(event, context)) | ||
const invocation = { event, context, dependencies } | ||
return Promise.resolve(invocation) | ||
.then(({ event, context }) => ({ | ||
payload: payloadFactory(event, context), | ||
dependencies | ||
})) | ||
.then(appConstructor) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { environment } from '../src' | ||
describe('serverless environment', () => { | ||
// here we will have adapter | ||
// but for testing it should be enough for time being | ||
|
||
type EmptyEvent = {} | ||
type EmptyContext = {} | ||
it('supports application', () => | ||
environment<EmptyEvent, EmptyContext, void, void, string>() | ||
.app(() => 'hello world!') | ||
.start({}, {}) | ||
.then(result => expect(result).toBe('hello world!'))) | ||
|
||
type MessageEvent = { message: string } | ||
type NameContext = { name: string } | ||
type EventPayload = { event: MessageEvent; context: NameContext } | ||
it('supports passing an event and context to application', () => | ||
environment<MessageEvent, NameContext, void, EventPayload, string>() | ||
.app(({ payload: { event, context } }) => `${event.message} ${context.name}!`) | ||
.start({ message: 'hello' }, { name: 'world' }) | ||
.then(result => expect(result).toBe('hello world!'))) | ||
|
||
type BuildMessage = (message: string, name: string) => string | ||
type BuildMessageDependencies = { | ||
buildMessage: BuildMessage | ||
} | ||
it('supports adding dependencies to environment', () => { | ||
const buildMessage: BuildMessage = (message, name) => `${message} ${name}!` | ||
|
||
return environment<MessageEvent, NameContext, BuildMessageDependencies, EventPayload, string>() | ||
.global(buildMessage) | ||
.app(({ payload: { event, context }, dependencies: { buildMessage } }) => | ||
buildMessage(event.message, context.name) | ||
) | ||
.start({ message: 'hello' }, { name: 'world' }) | ||
.then(result => expect(result).toBe('hello world!')) | ||
}) | ||
|
||
it.todo('supports adding not named dependencies to environment') | ||
|
||
it('supports creating a payload for the application', () => { | ||
type HelloWorldMessage = { | ||
hello: string | ||
world: string | ||
} | ||
|
||
type BuildHelloWorldMessage = (message: HelloWorldMessage) => string | ||
|
||
const buildMessage: BuildHelloWorldMessage = message => `${message.hello} ${message.world}!` | ||
|
||
type BuildHelloWorldMessageDependencies = { | ||
buildMessage: BuildHelloWorldMessage | ||
} | ||
|
||
return environment<MessageEvent, NameContext, BuildHelloWorldMessageDependencies, HelloWorldMessage, string>() | ||
.global(buildMessage) | ||
.payload((event, context) => ({ | ||
hello: event.message, | ||
world: context.name | ||
})) | ||
.app(({ payload, dependencies: { buildMessage } }) => buildMessage(payload)) | ||
.start({ message: 'hello' }, { name: 'world' }) | ||
.then(result => expect(result).toBe('hello world!')) | ||
}) | ||
// google run against express | ||
// it('runs request / response environment', () => environment().run(request, response)) | ||
// it('runs google function environment', () => environment().run(event, context)) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "./dist", | ||
"declaration": true | ||
}, | ||
"include": ["src/**/*", "tests/**/*"], | ||
"exclude": ["dist/**/*"] | ||
} |
Oops, something went wrong.