-
Notifications
You must be signed in to change notification settings - Fork 5
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import { Table } from "./types" | ||
import { buildKeyArray } from "./util" | ||
|
||
export function generateTables(tables: Table[]): string { | ||
const tableCode = tables.map((table) => { | ||
|
@@ -24,13 +25,18 @@ function generateEncryptionBlacklist(table: Table): string { | |
.forEach((model) => { | ||
const [, pk] = model.keys.partitionKey | ||
const [, sk] = model.keys.sortKey | ||
encryptionBlacklistSet.add(pk.replace("$", "")) | ||
encryptionBlacklistSet.add(sk.replace("$", "")) | ||
|
||
const pks = buildKeyArray(model.keys.partitionKey[1]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like you can just use the |
||
const sks = buildKeyArray(model.keys.sortKey[1]) | ||
pks.forEach((key: string) => encryptionBlacklistSet.add(key)) | ||
sks.forEach((key: string) => encryptionBlacklistSet.add(key)) | ||
}) | ||
|
||
table.gsis.forEach(({ name, partitionKey, sortKey }) => { | ||
encryptionBlacklistSet.add(partitionKey.replace("$", "")) | ||
encryptionBlacklistSet.add(sortKey.replace("$", "")) | ||
const pk = buildKeyArray(partitionKey) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: would just inline to: |
||
const sk = buildKeyArray(sortKey) | ||
pk.forEach((key: string) => encryptionBlacklistSet.add(key)) | ||
sk.forEach((key: string) => encryptionBlacklistSet.add(key)) | ||
}) | ||
|
||
return JSON.stringify(Array.from(encryptionBlacklistSet)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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[]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm...we're threading this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style: all For inline lambdas, e.g. |
||
if (Array.isArray(key)) { | ||
return key.map((k) => k.replace("$", "")) | ||
} | ||
return key.replace("$", "") | ||
} | ||
|
||
export function buildKey(key: string | string[]) { | ||
const cleanKey = replaceKeyDollar(key); | ||
if (Array.isArray(cleanKey)) { | ||
const keys = cleanKey.map((k) => `"${k}"`).join(', ') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not clear on why we put a dollar sign on each key part, and then just remove the first one below. |
||
return `[${keys}]` | ||
} | ||
return `"${cleanKey.replace("$", "")}"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to do what your |
||
} | ||
|
||
export function buildKeyArray(key: string | string[]) { | ||
const keyOrKeys = replaceKeyDollar(key) | ||
if (Array.isArray(keyOrKeys)) { | ||
return keyOrKeys | ||
} | ||
return [keyOrKeys] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,21 @@ | ||
import { PartitionAndSortKey, PartitionKeyAndSortKeyPrefix } from "./keys" | ||
import { Table } from "./Table" | ||
import { TaggedModel } from "./types" | ||
import { TaggedModel, AtLeastOne } from "./types" | ||
import { key } from "aws-sdk/clients/signer" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't think you intended to import this? |
||
|
||
export class Model< | ||
T extends TaggedModel, | ||
U extends keyof T, | ||
V extends keyof T | ||
> { | ||
> { | ||
constructor( | ||
private table: Table, | ||
private partitionKeyPrefix: string, | ||
private partitionKeyField: U, | ||
private partitionKeyField: U | U[], | ||
private sortKeyPrefix: string, | ||
private sortKeyField: V, | ||
private sortKeyField: V | V[], | ||
readonly modelTag: string | ||
) {} | ||
) { } | ||
|
||
key( | ||
params: { [X in U]: string } & { [Y in V]: string } | ||
|
@@ -27,10 +28,36 @@ export class Model< | |
} = this | ||
return new PartitionAndSortKey( | ||
this.table.partitionKeyName, | ||
this.buildKey(partitionKeyPrefix, params[partitionKeyField]), | ||
this.buildKey( | ||
partitionKeyPrefix, | ||
this._buildPartitionKey(params, partitionKeyField)), | ||
|
||
this.table.sortKeyName, | ||
this.buildKey(sortKeyPrefix, params[sortKeyField]), | ||
this.buildKey( | ||
sortKeyPrefix, | ||
this._buildSortKey(params, sortKeyField)), | ||
this.modelTag | ||
) | ||
} | ||
|
||
partialKey( | ||
params: AtLeastOne<{ [X in U]: string }> & AtLeastOne<{ [Y in V]: string }> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
This is tricky to do as is since:
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 |
||
): PartitionKeyAndSortKeyPrefix<T> { | ||
const { | ||
partitionKeyPrefix, | ||
sortKeyPrefix, | ||
partitionKeyField, | ||
sortKeyField, | ||
} = this | ||
return new PartitionKeyAndSortKeyPrefix( | ||
this.table.partitionKeyName, | ||
this.buildKey( | ||
partitionKeyPrefix, | ||
this._buildPartitionKey(params, partitionKeyField)), | ||
this.table.sortKeyName, | ||
this.buildKey( | ||
sortKeyPrefix, | ||
this._buildSortKey(params, sortKeyField)), | ||
this.modelTag | ||
) | ||
} | ||
|
@@ -39,7 +66,9 @@ export class Model< | |
const { partitionKeyPrefix, partitionKeyField } = this | ||
return new PartitionKeyAndSortKeyPrefix( | ||
this.table.partitionKeyName, | ||
this.buildKey(partitionKeyPrefix, params[partitionKeyField]), | ||
this.buildKey( | ||
partitionKeyPrefix, | ||
this._buildPartitionKey(params, partitionKeyField)), | ||
this.table.sortKeyName, | ||
this.sortKeyPrefix, | ||
this.modelTag | ||
|
@@ -59,9 +88,10 @@ export class Model< | |
|
||
const pk = this.buildKey( | ||
partitionKeyPrefix, | ||
fieldsWithTag[partitionKeyField] | ||
) | ||
const sk = this.buildKey(sortKeyPrefix, fieldsWithTag[sortKeyField]) | ||
this._buildPartitionKey(fieldsWithTag, partitionKeyField)) | ||
const sk = this.buildKey( | ||
sortKeyPrefix, | ||
this._buildSortKey(fieldsWithTag, sortKeyField)) | ||
|
||
return { | ||
...fieldsWithTag, | ||
|
@@ -70,13 +100,49 @@ export class Model< | |
} | ||
} | ||
|
||
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[] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
if (Array.isArray(fields)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
const pkey: string[] = []; | ||
for (const field of fields) { | ||
const value = model[field] | ||
if (value) { | ||
pkey.push(value) | ||
} else { | ||
break | ||
} | ||
}; | ||
return pkey | ||
} | ||
return model[fields as U] | ||
} | ||
|
||
private _buildSortKey(model: { [Y in V]: string }, fields: V | V[]): string | string[] { | ||
if (Array.isArray(fields)) { | ||
const skey: string[] = []; | ||
for (const field of fields) { | ||
const value = model[field] | ||
if (value) { | ||
skey.push(value) | ||
} else { | ||
break | ||
} | ||
}; | ||
return skey | ||
} | ||
return model[fields as V] | ||
} | ||
|
||
private buildKey(prefix: string, key: string | string[]): string { | ||
if (Array.isArray(key)) { | ||
return `${prefix}-${key.join('-')}` | ||
} | ||
return `${prefix}-${key}` | ||
} | ||
} | ||
|
||
export class PartitionKeyBuilder<T extends TaggedModel> { | ||
constructor(private table: Table, private modelTag: string) {} | ||
constructor(private table: Table, private modelTag: string) { } | ||
partitionKey<U extends keyof T>( | ||
prefix: string, | ||
partitionKeyField: U | ||
|
@@ -94,11 +160,11 @@ export class SortKeyBuilder<T extends TaggedModel, U extends keyof T> { | |
constructor( | ||
private table: Table, | ||
private partitionKeyPrefix: string, | ||
private partitionKeyField: U, | ||
private partitionKeyField: U | U[], | ||
private modelTag: string | ||
) {} | ||
) { } | ||
|
||
sortKey<V extends keyof T>(prefix: string, sortKeyField: V): Model<T, U, V> { | ||
sortKey<V extends keyof T>(prefix: string, sortKeyField: V | V[]): Model<T, U, V> { | ||
return new Model( | ||
this.table, | ||
this.partitionKeyPrefix, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -322,3 +322,49 @@ Tables: | |
|
||
expect(lines).toContainEqual(`name: BestNameEvah`) | ||
}) | ||
|
||
it("should generate a complex key model", () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice test, thanks! |
||
const result = generateCode(` | ||
Tables: | ||
ComplexLibrary: | ||
Partitions: | ||
ComplexAuthors: | ||
ComplexAuthor: | ||
partitionKey: [Author, $id] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
sortKey: [Author, [$id, $name]] | ||
id: string | ||
name: string | ||
`) | ||
|
||
expect(result).toEqual(`import { Table } from "@ginger.io/beyonce" | ||
|
||
export const ComplexLibraryTable = new Table({ | ||
name: "ComplexLibrary", | ||
partitionKeyName: "pk", | ||
sortKeyName: "sk", | ||
encryptionBlacklist: ["id", "name"] | ||
}) | ||
|
||
export enum ModelType { | ||
ComplexAuthor = "ComplexAuthor" | ||
} | ||
|
||
export interface ComplexAuthor { | ||
model: ModelType.ComplexAuthor | ||
id: string | ||
name: string | ||
} | ||
|
||
export const ComplexAuthorModel = ComplexLibraryTable.model<ComplexAuthor>( | ||
ModelType.ComplexAuthor | ||
) | ||
.partitionKey("Author", "id") | ||
.sortKey("Author", ["id", "name"]) | ||
|
||
export type Model = ComplexAuthor | ||
|
||
export const ComplexAuthorsPartition = ComplexLibraryTable.partition([ | ||
ComplexAuthorModel | ||
]) | ||
`) | ||
}) |
There was a problem hiding this comment.
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?