Skip to content
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

Partial Key Queries #33

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft

Conversation

cjoelrun
Copy link
Contributor

@cjoelrun cjoelrun commented Jun 8, 2020

Looking to get some feedback on approach and thoughts about how to make partialKey queries safer.

@cjoelrun cjoelrun requested a review from jcarver989 June 8, 2020 23:40
}

partialKey(
params: AtLeastOne<{ [X in U]: string }> & AtLeastOne<{ [Y in V]: string }>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where I think I could possibly use a generated type of only valid combinations of keys.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I think the types we'd want to express here would be a union like:

{ country: string } | { country: string, state: string } | { country: string, state: string, city: string }

This is tricky to do as is since:

  1. We have a generic model class
  2. All it knows inside here is that partitionKeys / sortKeys are a union type.
  3. Afaik there's not a way to extract keys of a union type in-order and re-combine them to what we'd want (maybe there is, I just don't know about it).

This would be easier to express if we explicitly codegenned each model vs having a generic model class as we do today. Since then, we could just codegen the right types directly

@cjoelrun cjoelrun marked this pull request as draft June 8, 2020 23:47
@jcarver989
Copy link
Contributor

Nice @cjoelrun ! Going to try to get to reviewing this ~soon.

@cjoelrun
Copy link
Contributor Author

I think there's still some work to be done, just wanted your opinion before I continue in this or another direction.

Copy link
Contributor

@jcarver989 jcarver989 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice @cjoelrun !

Sorry it took so long to get to this PR. But really exciting to see!

I think the overall approach here is a good short-term solution, in that we can check-in an initial version of complex / partial key support that doesn't enforce needing correct combinations of partial keys. As a "v1 / mvp" of this feature we could just allow a Partial<T> where T is the "full key".

Then, the long-term approach I'd want to take is to refactor our Model class / codegen to make expressing the types needed to enforce correct partial key combinations easier. I think this will basically result in axe-ing our generic Model class and generating specific classes for each model instead.

So, for now:

  1. I'd like to DRY-up a lot of the isArray / isNotArray checks that are a result of threading the string | string[] type throughout the code. I think we could do this pretty easily by just normalizing non-array types to arrays near the "top" of where they appear.

  2. There's a few small things related to style/naming that I've left comments on.

@@ -1,4 +1,5 @@
import { Fields, Table } from "./types"
import {buildKey, replaceKeyDollar} from "./util"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports seem not be used?

encryptionBlacklistSet.add(pk.replace("$", ""))
encryptionBlacklistSet.add(sk.replace("$", ""))

const pks = buildKeyArray(model.keys.partitionKey[1])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you can just use the pk and sk variables above as they're already bound to model.keys.partitionKey[1] and model.keys.sortKey[1]

})

table.gsis.forEach(({ name, partitionKey, sortKey }) => {
encryptionBlacklistSet.add(partitionKey.replace("$", ""))
encryptionBlacklistSet.add(sortKey.replace("$", ""))
const pk = buildKeyArray(partitionKey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: would just inline to: buildKeyArray(partitionKey).forEach(key => encryptionBlacklist.add(key))

@@ -11,3 +11,27 @@ export function groupBy<T extends { [key: string]: any }, U extends keyof T>(

return Object.entries(groupedItems)
}

export function replaceKeyDollar(key: string | string[]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm...we're threading this string | string[] type through multiple places in the code. I'd prefer we only do this check once at the "top level" and just transform string types into string[]. That way, everything that comes after can just assume the type is string[]

@@ -11,3 +11,27 @@ export function groupBy<T extends { [key: string]: any }, U extends keyof T>(

return Object.entries(groupedItems)
}

export function replaceKeyDollar(key: string | string[]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: all function types outside of tests should explicitly specify their return types. This avoids accidentally getting the wrong type inferred.

For inline lambdas, e.g. const f = () => { ... }, it's fine to leave the return type off since they're usually very simple .

private buildKey(prefix: string, key: string): string {
/* TODO: Had issue mapping unioned fields of T & V */
private _buildPartitionKey(model: AtLeastOne<{ [X in U]: string }>, fields: U | U[]): string | string[] {
if (Array.isArray(fields)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is another place we're we'd benefit from normalizing non-array keys into arrays. For example, we could transform keys to arrays internally in the model's constructor.

}

partialKey(
params: AtLeastOne<{ [X in U]: string }> & AtLeastOne<{ [Y in V]: string }>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I think the types we'd want to express here would be a union like:

{ country: string } | { country: string, state: string } | { country: string, state: string, city: string }

This is tricky to do as is since:

  1. We have a generic model class
  2. All it knows inside here is that partitionKeys / sortKeys are a union type.
  3. Afaik there's not a way to extract keys of a union type in-order and re-combine them to what we'd want (maybe there is, I just don't know about it).

This would be easier to express if we explicitly codegenned each model vs having a generic model class as we do today. Since then, we could just codegen the right types directly

@@ -322,3 +322,49 @@ Tables:

expect(lines).toContainEqual(`name: BestNameEvah`)
})

it("should generate a complex key model", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test, thanks!

Partitions:
ComplexAuthors:
ComplexAuthor:
partitionKey: [Author, $id]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would like to add a test for partition keys as well, since it looks like this PR supports complex keys for both.

@@ -11,3 +11,5 @@ export type ExtractKeyType<T> = T extends PartitionAndSortKey<infer U>
export type GroupedModels<T extends TaggedModel> = {
[K in T["model"]]: T extends { model: K } ? T[] : never
}

export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... This ends up generating a pretty crazy type that's hard to read. And it appears to be basically equivalent to Partial<{ [X in U]: string }> -- i.e. you can specify one or more of the key parts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants