Skip to content

Instantly share code, notes, and snippets.

@tywalch
Last active August 25, 2020 17:03
Show Gist options
  • Save tywalch/8040087e0fc886ca5f742aa99b623e1b to your computer and use it in GitHub Desktop.
Save tywalch/8040087e0fc886ca5f742aa99b623e1b to your computer and use it in GitHub Desktop.
Koan example in ElectroDB
// 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