Published
Edited
Sep 20, 2021
3 forks
1 star
Insert cell
Insert cell
Insert cell
sc = require("sourcecred@latest")
// In other environments, you might need to use:
// sc = require("sourcecred").sourcecred
Insert cell
Insert cell
declaration = {
// We strongly recommend that your base prefixes follow the convention of "your org" + "your plugin"
// It is important that your plugin prefixes are unique so that there are no addressing conflicts
// with other plugins that might be in the graph.
const nodePrefix = sc.core.graph.NodeAddress.fromParts(["MyOrg", "MyPlugin"]);
const edgePrefix = sc.core.graph.EdgeAddress.fromParts(["MyOrg", "MyPlugin"]);

const mealNodeType = {
name: "meal",
pluralName: "meals",
prefix: sc.core.graph.NodeAddress.append(nodePrefix, "MEAL"),
defaultWeight: 1,
description: "A meal made and served in the restaurant"
};
const tableNodeType = {
name: "table",
pluralName: "tables",
prefix: sc.core.graph.NodeAddress.append(nodePrefix, "TABLE"),
defaultWeight: 0,
description: "A table in the restaurant"
};
const serverNodeType = {
name: "server",
pluralName: "servers",
prefix: sc.core.graph.NodeAddress.append(nodePrefix, "SERVER"),
defaultWeight: 0,
description: "A server working in the restaurant"
};
const chefNodeType = {
name: "chef",
pluralName: "chefs",
prefix: sc.core.graph.NodeAddress.append(nodePrefix, "CHEF"),
defaultWeight: 0,
description: "A chef working in the restaurant"
};

const cookedEdgeType = {
forwardName: "cooked",
backwardName: "was cooked by",
prefix: sc.core.graph.EdgeAddress.append(edgePrefix, "COOKED"),
defaultWeight: { forwards: 0, backwards: 1 },
description: "Connects a Chef to a Meal they cooked."
};
const deliveredToEdgeType = {
forwardName: "was delivered to",
backwardName: "received",
prefix: sc.core.graph.EdgeAddress.append(edgePrefix, "DELIVERED_TO"),
defaultWeight: { forwards: 1, backwards: 0 },
description: "Connects a Meal to a Table it was delivered to."
};
const servedEdgeType = {
forwardName: "served",
backwardName: "was served by",
prefix: sc.core.graph.EdgeAddress.append(edgePrefix, "SERVED"),
defaultWeight: { forwards: 0, backwards: 1 },
description: "Connects a Server to a Table that they served."
};

return {
name: "Restaurant demo (external)",
nodePrefix: nodePrefix,
nodeTypes: [mealNodeType, tableNodeType, serverNodeType, chefNodeType],
edgePrefix: edgePrefix,
edgeTypes: [cookedEdgeType, deliveredToEdgeType, servedEdgeType],
userTypes: [serverNodeType, chefNodeType]
};
}
Insert cell
Insert cell
createWeightedGraph = () => {
const graph = new sc.core.graph.Graph();
const weights = sc.core.weights.empty();

for (const meal of mockApi.getMeals()) {
graph.addNode({
address: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"MEAL", // Make sure you're prefixing how you said you would in the Declaration
meal.id
),
description: `A beautiful ${meal.name}`,
timestampMs: meal.timestamp
});

// Now we start branching to related data.
// This ensures that chefs are only added if they made a meal,
// so that the graph doesn't get too cluttered.
// We could include all chefs though. Up to us.
const chef = mockApi.getChefById(meal.chefId);
graph.addNode({
address: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"CHEF",
chef.id
),
description: `A chef named ${chef.name}`,
timestampMs: null // Set null if not applicable
});

// Both meals have the same Table and Server!
// Fortunately, graph.addNode and graph.addEdge will be idempotent
// even if we try to add the same node or edge twice, as long as the node/edge
// is built exactly the same every time.
const table = mockApi.getTableById(meal.tableId);
graph.addNode({
address: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"TABLE",
table.id
),
description: `Sturdy table ${table.id}`,
timestampMs: null
});
const server = mockApi.getServerById(table.serverId);
graph.addNode({
address: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"SERVER",
server.id
),
description: `A server named ${server.name}`,
timestampMs: null
});

// We've made all the important nodes for this meal,
// now let's connect them with edges!
graph.addEdge({
address: sc.core.graph.EdgeAddress.append(
declaration.edgePrefix, // Notice, we're using EdgeAddress and edgePrefix here!
"COOKED",
chef.id,
meal.id
),
timestamp: meal.timestamp,
// Pro tip: Helper functions for constructing each address type from an id/object can be really useful
// for reducing bugs and duplicate code. Otherwise, we'll be doing a lot of this:
src: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"CHEF",
chef.id
),
// Make sure the src -> dst match the "forwardName" semantics in the declaration.
dst: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"MEAL",
meal.id
)
});
graph.addEdge({
address: sc.core.graph.EdgeAddress.append(
declaration.edgePrefix,
"DELIVERED_TO",
meal.id,
table.id
),
timestamp: meal.timestamp, // We don't know exactly when it was delivered, so we're approximating.
src: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"MEAL",
meal.id
),
dst: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"TABLE",
table.id
)
});
graph.addEdge({
address: sc.core.graph.EdgeAddress.append(
declaration.edgePrefix,
"SERVED",
server.id,
table.id
),
timestamp: null,
src: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"SERVER",
server.id
),
dst: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"TABLE",
table.id
)
});

// Whew! We're almost there.
// If you want to get fancy, you can create custom per-node or per-edge weights
// by mapping addresses to weights.
weights.nodeWeights.set(
sc.core.graph.NodeAddress.append(declaration.nodePrefix, "MEAL", meal.id),
// This will make the weight of each meal directly proportional to "how much money it costs."
meal.cost
);

// Let's get real fancy and add some logic!
if (chef.rank === "JUNIOR") {
// Depending on how you created your address declarations, you can also add weight multipliers
// to node or edge address prefixes. This might affect how you want to order address parts.
// See how this is just a prefix which will match all COOKED edges for the current chef.
weights.edgeWeights.set(
sc.core.graph.EdgeAddress.append(
declaration.edgePrefix,
"COOKED",
chef.id
),
{
// Since we are adding a prefix MULTIPLIER, we set 1 to indicate "no change"
forwards: 1,
// Semantically, reducing the edge weight here means less cred will flow from meals to JUNIOR chefs.
// Instead, more cred will flow out from other edges, in this case from meals to servers.
// Maybe a little strange semantically for servers to get more cred when serving meals made
// by junior chefs, so I might not build the graph this way in a real scenario. Then again, the
// servers have to deal with unhappy customers when the food is worse, so maybe this does make
// sense! The lesson is, although some configurability is given to the Instance Admins through
// the Declaration, we as devs have a LOT of power to influence cred by how we choose to
// build and alter graphs.
backwards: 1 / 2
}
);
}

// I created Nodes, then Edges, then Weights, but you can mix it up and add
// things however makes sense for the APIs and data you're working with.
}
return { graph, weights };
}
Insert cell
weightedGraph = createWeightedGraph()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
createIdentityProposals = () => {
const result = [];
// pluginName can be anything, but must only contain letters, numbers, and dashes
const pluginName = "Restaurant-plugin";
for (const chef of mockApi.getChefs()) {
result.push({
// "name" can only contain letters, numbers, and dashes
// You will have to clean your data if it does not already follow that requirement.
// We will soon have a helper function exposed to do that.
name: chef.name,
pluginName,
type: "USER", // The options are USER, BOT, ORGANIZATION, PROJECT
alias: {
description: `restaurant chef ${chef.name}`,
address: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"CHEF",
chef.id
)
}
});
}
for (const server of mockApi.getServers()) {
result.push({
name: server.name,
pluginName,
type: "USER",
alias: {
description: `restaurant server ${server.name}`,
address: sc.core.graph.NodeAddress.append(
declaration.nodePrefix,
"SERVER",
server.id
)
}
});
}
return result;
}
Insert cell
identityProposals = createIdentityProposals()
Insert cell
Insert cell
Insert cell
// should not error
{
sc.plugins.identityProposalsParser.parseOrThrow(identityProposals);
sc.plugins.declarationParser.parseOrThrow(declaration);
}
Insert cell
Insert cell
graphInput = ({
plugins: [
{
// Use the ConstructorPlugin to pipe your structures into this plugin field. Leave the rest.
plugin: new sc.plugins.ConstructorPlugin({
graph: weightedGraph,
declaration: declaration,
identityProposals: identityProposals
}),
directoryContext: null,
pluginId: "doesnt/matter"
}
],
ledger: new sc.ledger.ledger.Ledger()
})
Insert cell
// should not error
graphOutput = sc.api.graph.graph(graphInput)
Insert cell
Insert cell
credrankInput = ({
pluginGraphs: [weightedGraph], // Pass your weightedGraph here, keep the rest as is.
ledger: graphOutput.ledger, // Important to use the same ledger that was generated by your graph API test
dependencies: [],
weightOverrides: sc.core.weights.empty(),
pluginsBudget: null,
personalAttributions: []
})
Insert cell
// should not error
credrankOutput = sc.api.credrank.credrank(credrankInput)
Insert cell
Insert cell
credrankOutput.credGrainView
.participants()
.map(p => `Name: ${p.identity.name}\nTotal Score: ${p.cred}`)
.join('\n\n')
Insert cell
Insert cell
serializedWeightedGraph = JSON.stringify(
sc.core.weightedGraph.toJSON(weightedGraph) // Notice, this is a little different than the other two.
)
Insert cell
serializedDeclaration = JSON.stringify(declaration)
Insert cell
serializedIdentityProposals = JSON.stringify(identityProposals)
Insert cell
Insert cell
Insert cell
mockApi = ({
getMeals: () => [
{
id: "1",
name: "Bread-bowl Soup",
cost: 20,
tableId: "1",
chefId: "3",
timestamp: new Date().getTime()
},
{
id: "2",
name: "Grilled Cheese",
cost: 22,
tableId: "2",
chefId: "4",
timestamp: new Date().getTime()
}
],
getTableById: id => ({ id: id, serverId: "15" }),
getServerById: id => {
return new Map([["15", { id: "15", name: "Garnet-Server" }]]).get(id);
},
getChefById: id => {
return new Map([
["3", { id: "3", name: "Sapphire-Senior-Chef", rank: "SENIOR" }],
["4", { id: "4", name: "Ruby-Junior-Chef", rank: "JUNIOR" }]
]).get(id);
},
getChefs: () => [
{ id: "3", name: "Sapphire-Senior-Chef", rank: "SENIOR" },
{ id: "4", name: "Ruby-Junior-Chef", rank: "JUNIOR" }
],
getServers: () => [{ id: "15", name: "Garnet-Server" }]
})
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more