Last active
August 25, 2020 17:03
-
-
Save tywalch/8040087e0fc886ca5f742aa99b623e1b to your computer and use it in GitHub Desktop.
Koan example in ElectroDB
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
// Modeling the example posed by Ashwin Bhat in ElectroDB | |
// https://medium.com/developing-koan/modeling-graph-relationships-in-dynamodb-c06141612a70 | |
const {Entity, Service} = require("electrodb"); | |
const Roles = { | |
"lead": "500", | |
"contributor": "400", | |
"team": "300", | |
} | |
let goal = { | |
entity: "goal", | |
attributes: { | |
goalId: { | |
type: "string", | |
label: "g" | |
}, | |
edgeSet: "any", // Soon to be "set" | |
title: "string" | |
}, | |
indexes: { | |
goal: { | |
collection: "goals", | |
pk: { | |
field: "source", | |
facets: ["goalId"] | |
}, | |
sk: { | |
field: "target", | |
facets: ["goalId"] | |
} | |
} | |
} | |
}; | |
let membership = { | |
entity: "membership", | |
attributes: { | |
userId: { | |
type: "string", | |
label: "user" | |
}, | |
goalId: { | |
type: "string", | |
label: "g" | |
}, | |
type: { | |
type: ["user", "team"] | |
}, | |
role: { | |
type: "string", | |
set: (role) => Roles[role && role.toLowerCase()], | |
get: (code) => { | |
let [role] = Object.entries(Role).find(pair => pair[1] === code); | |
return code; | |
}, | |
validate: (role) => { | |
if (Roles[role && role.toLowerCase()] === undefined) { | |
throw new Error(`acceptable values include ${Object.keys(Roles).join(", ")}`); | |
} | |
} | |
} | |
}, | |
indexes: { | |
goal: { | |
collection: "goals", | |
pk: { | |
field: "source", | |
facets: ["goalId"] | |
}, | |
sk: { | |
field: "target", | |
facets: ["type", "userId"] | |
} | |
}, | |
roles: { | |
index: "gsi0", | |
pk: { | |
field: "gsi0pk", | |
facets: ["type", "userId"] | |
}, | |
sk: { | |
field: "gsi0sk", | |
facets: ["role"] | |
} | |
} | |
} | |
}; | |
let koan = new Service({ | |
service: "koan", | |
table: "koantable", | |
}); | |
koan.join(goal); | |
koan.join(membership); | |
const goalId = "G1"; | |
const userId = "U1"; | |
const type = "user"; | |
const role = "lead"; | |
const title = "Successfully deliver GTM efforts in Asia" | |
koan.entities.goal.put({goalId, title}).params(); | |
// Put Goal: | |
// This could also use `.create()` which would prevent upserting the record. | |
// { | |
// Item: { | |
// goalId: 'G1', | |
// title: 'Successfully deliver GTM efforts in Asia', | |
// source: '$koan_1#g_g1', | |
// target: '$goals#goal#g_g1', | |
// __edb_e__: 'goal' | |
// }, | |
// TableName: 'koantable' | |
// } | |
koan.entities.membership.put({userId, goalId, type, role}).params(); | |
// Put Membership: | |
// This could also use `.create()` which would prevent upserting the record. | |
// { | |
// Item: { | |
// userId: 'U1', | |
// goalId: 'G1', | |
// type: 'user', | |
// role: '500', | |
// source: '$koan_1#g_g1', | |
// target: '$goals#membership#type_user#user_u1', | |
// gsi0pk: '$koan_1#type_user#user_u1', | |
// gsi0sk: '$membership#role_500', | |
// __edb_e__: 'membership' | |
// }, | |
// TableName: 'koantable' | |
// } | |
koan.entities.goal.get({goalId}).params(); | |
// Get Goal: | |
// { | |
// Key: { source: '$koan_1#g_g1', target: '$goals#goal#g_g1' }, | |
// TableName: 'koantable' | |
// } | |
koan.entities.membership.query.goal({goalId}).params(); | |
// Get all Goal Members: | |
// { | |
// KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)', | |
// TableName: 'koantable', | |
// ExpressionAttributeNames: { '#pk': 'source', '#sk1': 'target' }, | |
// ExpressionAttributeValues: { ':pk': '$koan_1#g_g1', ':sk1': '$goals#membership#type_' } | |
// } | |
console.log(koan.entities.membership.query.roles({type, userId}).between({role: Roles.lead}, {role: Roles.contributor}).params()); | |
// Get User's memberships by Role | |
// { | |
// TableName: 'koantable', | |
// ExpressionAttributeNames: { '#pk': 'gsi0pk', '#sk1': 'gsi0sk' }, | |
// ExpressionAttributeValues: { | |
// ':pk': '$koan_1#type_user#user_u1', | |
// ':sk1': '$membership#role_500', | |
// ':sk2': '$membership#role_400' | |
// }, | |
// KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2', | |
// IndexName: 'gsi0' | |
// } | |
koan.collections.goals({goalId}).params(); | |
// Get all Goal Details: | |
// Returns both the goal and it's memberships | |
// { | |
// KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)', | |
// TableName: 'koantable', | |
// ExpressionAttributeNames: { '#pk': 'source', '#sk1': 'target' }, | |
// ExpressionAttributeValues: { ':pk': '$koan_1#g_g1', ':sk1': '$goals' } | |
// } | |
// Note: | |
// There is definitely more that could be unlocked with going slightly different directions | |
// with their modeling. For starters, their example doesnt really need that GSI, it could be | |
// just another facet on `Target`. It's hard to see why the contribution type warrents it's own | |
// GSI, but then again I'm sure this is simply an example. I would imagine they'd have a User | |
// entity elsewhere, which I'm not sure how they tie the two together given the schema in their | |
// example. | |
// | |
// If you'd like I can show you how their use of that Edge Set might not even been | |
// neccessary depending on how they model their facets. This sorta gets to the core of why I | |
// made electro, most of using dynamo effectively is just being able to build the actual keys | |
// so you can get the most out of them before requiring a GSI or storing data in two different | |
// records. That being said there is some trade-offs to decide when building these keys, I could | |
// also go into. | |
// | |
// Here is an example how that might work: | |
let membership2 = new Entity({ | |
service: "koan", | |
table: "koantable", | |
entity: "membership2", | |
attributes: { | |
userId: { | |
type: "string", | |
label: "user" | |
}, | |
goalId: { | |
type: "string", | |
label: "g" | |
}, | |
type: { | |
type: ["user", "team"] | |
}, | |
role: { | |
type: "string", | |
set: (role) => Roles[role && role.toLowerCase()], | |
get: (code) => { | |
let [role] = Object.entries(Role).find(pair => pair[1] === code); | |
return code; | |
}, | |
validate: (role) => { | |
if (Roles[role && role.toLowerCase()] === undefined) { | |
throw new Error(`acceptable values include ${Object.keys(Roles).join(", ")}`); | |
} | |
} | |
} | |
}, | |
indexes: { | |
goal: { | |
collection: "goals", | |
pk: { | |
field: "source", | |
facets: ["goalId"] | |
}, | |
sk: { | |
field: "target", | |
facets: ["type", "role", "userId"] // order here would be defined by the needs of the app | |
} | |
} | |
} | |
}); | |
// Example One: | |
// sk facets = ["type", "role", "userId"] | |
// Get all "Users" that either "Lead" or "Contribute" to "G1" | |
membership2.query.goal({goalId, type}).between({role: Roles.lead}, {role: Roles.contributor}).params(); | |
// { | |
// TableName: 'koantable', | |
// ExpressionAttributeNames: { '#pk': 'source', '#sk1': 'target' }, | |
// ExpressionAttributeValues: { | |
// ':pk': '$koan_1#g_g1', | |
// ':sk1': '$goals#membership2#type_user#role_500#user_', | |
// ':sk2': '$goals#membership2#type_user#role_400#user_' | |
// }, | |
// KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2' | |
// } | |
// Example Two: | |
// sk facets = ["type", "userId", "role"] | |
// Get all contributions where the user is either a "Lead" or "Contributor" | |
membership2.query.goal({goalId, type}).between({role: Roles.lead}, {role: Roles.contributor}).params(); | |
// { | |
// TableName: 'koantable', | |
// ExpressionAttributeNames: { '#pk': 'source', '#sk1': 'target' }, | |
// ExpressionAttributeValues: { | |
// ':pk': '$koan_1#g_g1', | |
// ':sk1': '$goals#membership2#type_user#role_500#user_u1', | |
// ':sk2': '$goals#membership2#type_user#role_400#user_u1' | |
// }, | |
// KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2' | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment