Open pocesar opened 4 years ago
Oh hey! Sorry, I haven't been checking this in a while - thanks for trying punchcard and providing feedback.
You're absolutely right that Build
needs more documentation. For now, Build is built on fp-ts's Monad: https://gcanti.github.io/fp-ts/introduction/core-concepts.html so perhaps their documentation can help.
The way I think about Build is that it lazily expresses computations that happen in the Build-time phase of an application. So, to interact with existing CDK code, you only need to "map into" existing Build context or create one for yourself.
E.g. without using `Core.App:
const app = Build.lazy(() => new cdk.App())
const stack = app.map((app: cdk.App) => new cdk.Stack(app, 'my-stack'));
This is a lot like ordinary CDK code:
const app = new cdk.App();
const stack = new cdk.Stack(app, 'my-stack');
Except that execution of the CDK code is delayed until the entire Build
tree is executed during CDK synth. This layer is designed so that arbitrarily complex CDK code can be used without impacting the code that runs in Lamdba - e.g. interacting with the OS to create files or build code and Docker images etc. Before Build
existed, everything was executed all the time - so your lambda function would be calling things like new iam.Role()
and table.grantRead(role)
, which are wasteful and fragile when running within the Lambda Function. Things liek CDK Assets, for example, would break punchcard because they synchronously call out to the file system :(
In other words: Build
and Run
improve the portability and interoperability between Punchcard and the CDK by encapsulating computations lazily.
RE: the difference between map
and chain
:
class Build<T> {
<U> map(f: (t: T) => U): Build<U>;
<U> chain(f: (t: T) => Build<U>): Build<U>;
// chain is also known as flatMap in other functional languages, like Scala or Haskell
// even Java's Stream (which is also a Monad) has flatMap: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#flatMap-java.util.function.Function-
<U> flatMap(f: (t: T) => Build<U>): Build<U>;
}
Use map
when you don't need to interact with any other Build
instances. E.g. creating the Stack from the App (above) didn't need chain
because it can create the Stack from the App instance passed to its map function.
Use chain
when you need to link with other lazily constructed values.
const myTable: Build<dynamodb.Table>;
const myRole: Build<iam.Role>;
const myFunction: Build<lambda.Function> myRole.chain(role => myTable.map(table =>
const f = new lambda.Function(..,..,{
role
});
table.grantRead(f);
return f;
))
If we used map here instead of chain, then the type of myFunction
would be Build<Build<lambda.Function>>
. chain
flattened that for us, which is also why it's often called flatMap
.
awesome, thanks, I'm already publishing stuff about punchcard even if it's considered unstable and testing it for an MVP like I said (I just can't make it public atm, but I'll eventually), but that's how eagerly excited I am about the whole concept (CDK itself already kick-started my interest). the Build / Run is pure genius, I might add.
okay, I tried to do the following:
import getResource from './mycode'
/*...*/
const api = stack.map((s) => {
const certificate = Certificate.fromCertificateArn(
s,
'certificate',
certificateArn
)
return new p.ApiGateway.Api(stack, 'api', {
domainName: {
domainName: 'domain',
certificate
}
})
})
const res = Build.resolve(api).addResource('res')
res.setGetMethod({
integration: endpoint,
responses: {
/*...*/
},
request: {
/*...*/
},
handle: async (r) => {
try {
return p.ApiGateway.response(p.ApiGateway.StatusCode.Ok, {
result: await getResource(r.code)
})
} catch (e) {
return p.ApiGateway.response(p.ApiGateway.StatusCode.InternalError, {
result: null,
error: e.message
})
}
}
})
obviously didn't work, because I can't use Build.resolve
outside of map/chain
(and fails when deployed with attempted to resolve a Build value at runtime
which is totally true (awesome error reporting by the way 👍 ). should I put everything inside? Because if I use chain
, it gives me
to make it happy, add a Build.of
, but the end result is the same for when using map
. The reason I need the "api" variable is to assign it as an alias for a subdomain as a RecordSet
using AddressRecordTarget.fromAlias(new ApiGateway(Build.resolve(api.api)))
which works, for Route 53, it's in another stack.map
later on the code, but doesn't work for the Build value error above
my first attempt was putting Certificate.fromCertificateArn(Build.resolve(stack))
outside, alongside new p.ApiGateway.Api
, but the error is the same (and justifiably)
EDIT: ok, I think I see now how it should be. nothing from punchcard should go inside map, because it won't work at runtime, since map / lazy, aren't evaluated when running on lambda. it's making more sense the more I use it, but need some mental exercise
not related directly to the issue, but I ran across a documentation website generator, https://github.com/facebook/docusaurus might help with documenting this gem of a framework
Related: https://blog.axlight.com/posts/react-tracked-documentation-website-with-docusaurus-v2/ https://github.com/TypeStrong/typedoc
Sorry I haven't responded to your issue yet. It looks like you're getting confused by the Punchcard constructs p.ApiGateway.Api
and the CDK's.
p.ApiGateway.Api
should be constructed outside of the Build
and refrain from ever calling Build.resolve
, especially in the global module-load scope.
I don't have a better solution at this time though - I hope you don't mind waiting until I get round to re-implementing the API gateway stuff. It is by far the most neglected part of Punchcard right now.
although the PR #53 introduces the Core.App(), and using
app.root.map
idiom, it doesn't show to use the "mapped" stack to existing CDK code (as in using newnew cdk.Stack([what goes here now])
and what goes innew Bucket([here instead of Build<cdk.Stack>])
.Everything should go inside
map
? Or the return value frommap
should be used again usingmap((s) => new Bucket(s))
on the rest of the code? When to usechain
? What doesap
do?to be clear: the array method
map
gives the wrong impression thatroot.map
is an iterator with side effects, although it's just one item. but after reading the code, learned that map is actually a function that provides a param and outputs another wrapped Build.Also, would be good to export the inner
Build
type, just in case. (from the index main export)EDIT: after reading the internals in more-depth, I noticed that Build is really powerful, and while the abstractions of the AWS services are awesome, I think it deserves a "docs" for him, because it needs to get used to it to juggle around CDK constructs and Punchcard abstractions