Public
Edited
Jan 2, 2023
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],
keys: {
operatorKeys: [],
shareKeys: [],
weightKeys: [],
},
};
}
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({
weightedGraph, declaration, 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" }),
getChefs: () => [
{ id: "3", name: "Sapphire-Senior-Chef", rank: "SENIOR" },
{ id: "4", name: "Ruby-Junior-Chef", rank: "JUNIOR" }
],
getServers: () => [{ id: "15", name: "Garnet-Server" }],
getServerById(id) {
return new Map(
this.getServers().map((server) => [server.id, server])
).get(id);
},
getChefById(id) {
return new Map(
this.getChefs().map((chef) => [chef.id, chef])
).get(id);
},

})
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more