-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support GraphQL (AppSync) #86
Comments
Interesting code-first approach for GraphQL: https://github.com/MichalLytek/type-graphql |
Woah! I really like the look of type-graphql. Nice usage of decorators - something we haven't used in Punchcard yet. This might be a better way to define things like Shapes! I'm gonna see if Punchcard can integrate with its dependency injection framework. It would be amazing to have an end-to-end of a type-graphql app backed by the CDK. Then, if we can figure out how to do UI (integrate with React), then we'd have a full stack. |
decorators plus the generation of metadata from them are really powerful ❤️ and also fits perfectly for the shapes in general food for thought: create another example of how the full react stack should look like (like using Cloudfront, S3, and even Route53 with an "graphql" endpoint). this one will surely attract a lot of people to know more about how great is punchcard and CDK in general |
Just not quite sure how to generate the DSLs for things like DynamoDB if we move to annotated classes. E.g. const MyShape = struct({
key: string({maxLength: 1})
});
// vs
class MyShape {
@string({maxLength: 1})
public key: string;
} If we use const table = new DynamoDB.Table(..,.., {
attributes: MyShape,
partitionKey: 'key'
});
// and then the generated DSL:
await table.put({
item: ...,
if: item => item.key.equals('key')
}); Perhaps a JS proxy could help us here? We'd have to change how the type-mapping works. Shapes currently encode the "runtime shape" in the type: e.g. |
Here's a succinct way to integrate Shapes with Classes: const dataShape = Symbol();
class MyShape implements RuntimeShape<MyShape[typeof dataShape]> {
public readonly [dataShape] = struct({
key: string()
});
public key: string;
} |
Simplifying this further - remove the class MyShape {
@Annotation
public readonly key = string();
} Only caveat is that a runtime value is not an instance of this class, it is an instance of |
This seems really clean. export interface ClassType<T = any> {
new (...args: any[]): T;
}
type Value<T> = {
[prop in keyof T]:
T[prop] extends Shape ? RuntimeShape<T[prop]> :
T[prop] extends ClassType<infer V> ? Value<V> :
never;
};
class OtherType {
key = string();
}
class Rate {
rating = integer();
key = string();
otherType = OtherType;
nested = Rate;
}
const rate = new Rate();
const rateValue: Value<Rate>;
rateValue.key; // string
rateValue.otherType; // Value<OtherType>
rateValue.nested.key; // string Even supports recursive types by the looks of it. |
I was checking the struct-overhaul branch, it reminded me of how kotlin does json (de)serialization, the readability is really nice! even though the code itself is over my head, the generated metadata is really interesting and the use o Proxy while providing type-safety recursively without having to mutate the input 🥇 |
Do you have a link to an example of how Kotlin does it? It reminds me of Django/Google App Engine for Python, where class members are values that represent a type. https://docs.djangoproject.com/en/3.0/topics/db/models/ from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30) Except I de-coupled the constraints (such as An example of this in action: class MyType {
/**
* Field documentation.
*/
id = string
.apply(MaxLength(1))
.apply(MinLength(0))
.apply(Pattern('.*'));
// can be thought of like
// @MaxLength(1)
// @MinLength(0)
// @Pattern('.*)
// id = string
}
// the type of the derived schema retains all information, even the literal values
// such as maxLength: 1, instead of maxLength: number
// note: the type is inferred, I've explicitly included it here to illustrate
const schema: ObjectSchema<{
id: StringSchema<{
maxLength: 1;
minLength: 0;
pattern: ".*";
}>;
}> = JsonSchema.of(MyType); This issue is related: microsoft/TypeScript#7169 - support for the reification of generic types by the TS compiler for use with JS reflection. My plan for the The framework in its current state already supports all the features I need. I'm just in the middle of porting the existing work in punchcard to separate |
Some cool (potentially useful) type-machinery is the use of this metadata to build type-safe predicates: class MyType {
count = number
.apply(MultipleOf(2))
}
function requireEven(schema: NumberSchema<{multipleOf: 2}>) {
// this function requires the schema to represent even numbers
}
const schema = JsonSchema.of(MyType);
// compiles
requireEven(schema.properties.count); Compilation breaks if I now comment out the I wonder if we could do operations like I found this, https://github.com/thomaseding/nat-ts, not sure how useful it is though. |
This has turned out so much better than I'd expected. It's a generic, type-safe AST transformer. Shapes + Traits modeled in the TS compile-time type-system mean I can transform a Shape declaration into any other format (AST) without loss of type information metadata. In fact, that metadata information can even be used to influence the result. This is effectively a generic code generator that doesn't generate code - it generates types. Look how easy it is to generate code: class MyType {
/**
* Field documentation.
*/
id = string
.apply(MaxLength(1))
.apply(MinLength(0))
.apply(Pattern('.*'))
;
count = number
.apply(Maximum(1))
.apply(Minimum(1, true))
.apply(MultipleOf(2))
;
nested = Nested;
array = array(string);
complexArray = array(Nested);
set = set(string);
complexSet = set(Nested);
map = map(string);
complexMap = map(Nested);
}
// generate an interface representing this object at runtime
interface MyRuntimeRepresentation extends Runtime.OfType<MyType> {}
// generate an interface representing this object's JSON schema reprsentation
interface MyJsonSchemaRepresentation extends JsonSchema.OfType<MyType> {} Implementing a AST transformer is relatively simple too. This is all that's required to map a Shape to its JavaScript runtime value: |
well, I'm 5 minutes'ish late, but here's how kotlin + moshi does it https://proandroiddev.com/getting-started-using-moshi-for-json-parsing-with-kotlin-5a460bf3935a EDIT: gotta say, never knew you could reference "this" directly inside a interface like that plus access it like an object haha. while it DOES seem simple, after it's ready, I'm not being able to understand everything, I plan to study punchcard source thoroughly to be able to help, at least the thought process EDIT2: after checking some of the asserts, TS 3.7 have assert types now, might come in handy https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions it's the cousin of "arg is something", it's like a boolean on steroids function isString(val: any): val is string {
return typeof val === "string";
}
function yell(str: any) {
if (isString(str)) {
return str.toUppercase();
}
throw "Oops!";
} |
Came up with another approach with a key benefit: the class Instead of just having a class, we can const class = Symbol.for('class');
// psuedo code
export function data<T extends {[prop: string]: Shape;}>(shape: T): ClassType<{
// class contains a member for each of the values - awesome!
[K in keyof T]: Value.Of<T[K]>;
} & {
// put the shape on the class instance, like getClass() in Java.
[class]: Shape.Of<T>;
}> & {
// static reference to the class (shape data)
readonly class: Shape.Of<T>;
}{
// dynamically construct a class
class TheClass {}
TheClass.class = Shape.of(shape); // static class
TheClass.prototype.class = TheClass.class; // reference to class on the instance
return TheClass as any;
} Usage: class MyData extends data({ // we extend the result of the data function call, which is a dynamically generated class
key: string
.apply(Decorator) // traits can be dynamically applied like this, unlike TS decorators, because we use ordinary code that isn't restricted to only running on module-load
}) {
// i can implement methods on the class now!
public getKey(): string {
return this.key; // compiles
}
}
MyData.class; // contains the static information about this class and its members, like in Java. We've basically built a static type-system for TS.
// MyData is the representation at runtime, instead of the ugly Value.Of<typeof MyData>
const value = new MyData({
key: 'this is a key' // type-safe constructor
});
value.key; // of type: string
value[class]; // this instance's class. Only caveat is that you can't extend a "data class" to add new properties. // won't work, bummer.
class MyExtendedData extends MyData {
key = string; // won't be used
} This would align with Smithy's "no inheritance" tenet for DDLs. Maybe it's a good thing? Thoughts? |
If we went with this approach, reflection would be identical to Java: const queue = new SQS.Queue(,, MyData.class); // pass the .class instance - just like Java |
oh, good idea, it's also how kotlin does java interop as well by default, imo it's definitely a way forward, might be weird for JS/TS only coders though. side note: won't this clash with #100? |
Bundling (#100) refers to the running of webpack to create a minified JS file for running inside Lambda. It shouldn't clash with this change - the new Shape system is ordinary JS/TS. |
GraphQL has a really nice way of modeling APIs and Types and how they're fulfilled by many different backend calls (via adapters). This creates an interesting opportunity to leverage Punchcard's abstraction of
Build
andRun
time to build generic and re-usable APIs on top of diverse and complex AWS resources.The text was updated successfully, but these errors were encountered: