Social Bookmarking
We are going to set up a WeaveDB contract for a social bookmarking dapp.
The actual dapp with the same setup is deployed at asteroid.ac which let you bookmark and explore Mirror.xyz articles.
The implementation details will be omitted for this tutorial. You can go through the whole granular process of building both the contracts and the frontend dapp in the previous Todo Manager example.
Deploy WeaveDB Contracts
git clone https://github.com/weavedb/weavedb.git
cd weavedb
yarn
node scripts/generate-wallet.js mainnet
yarn deploy
Database Structure
We are going to set up 3 collections.
bookmarks
: user bookmarksmirror
: mirror articlesconf
: for some bookkeeping
Set up Data Schemas
const schemas_bookmarks = {
type: "object",
required: ["article_id", "date", "user_address"],
properties: {
article_id: {
type: "string",
},
user_address: {
type: "string",
},
date: {
type: "number",
},
},
}
await db.setSchema(schemas_bookmarks, "bookmarks")
const schemas_conf = {
type: "object",
required: ["ver"],
properties: {
ver: {
type: "number",
},
},
}
await db.setSchema(schemas_conf, "conf")
bookmarks
must have 3 fields (article_id
,data
,user_address
).conf
must havever
field.
Set up Access Control Rules
bookmarks collection
const rules_bookmarks = {
let: {
id: [
"join",
":",
[
{ var: "resource.newData.article_id" },
{ var: "resource.newData.user_address" },
],
],
},
"allow create": {
and: [
{
"==": [{ var: "resource.id" }, { var: "id" }],
},
{
"==": [
{ var: "request.auth.signer" },
{ var: "resource.newData.user_address" },
],
},
{
"==": [
{ var: "request.block.timestamp" },
{ var: "resource.newData.date" },
],
},
],
},
"allow delete": {
"==": [
{ var: "request.auth.signer" },
{ var: "resource.data.user_address" },
],
},
}
await db.setRules(rules_bookmarks, "bookmarks")
let
declaresid
variable and assignarticle_id:user_address
of thenewData
to it.- The doc
id
must bearticle_id:user_address
. user_address
must be txsigner
.data
must beblock.timestamp
- Only the setter of the record (
user_address
) can delete it.
mirror collection
const rules_mirror = {
"allow write": {
"==": [{ var: "request.auth.signer" }, ADMIN_ADDRESS],
},
}
await db.setRules(rules_mirror, "mirror")
mirror
articles will be recorded when calculating bookmark counts and only ADMIN_ADDRESS
can write.
conf collection
const rules_conf = {
"allow write": {
"==": [{ var: "request.auth.signer" }, ADMIN_ADDRESS],
},
}
await db.setRules(rules_conf, "conf")
conf
will be used for periodic bookmark counting and only ADMIN_ADDRESS
can write.
Bookmark Articles
The frontend dapp can implement the following query for bookmarking.
It forces date
to be the block timestamp, and user_address
to be the tx signer address which should be the same as USER_ADDRESS
.
await db.set(
{
date: db.ts(),
article_id: ARTICLE_ID,
user_address: db.signer(),
},
"bookmarks",
`${ARTICLE_ID}:${USER_ADDRESS}`
)
Calculate Bookmark Counts
Our dapp will show trending articles by periodically calculating bookmark counts.
We will use a time-decay algorithm to rank articles by the bookmarks in the past 2 weeks.
import {isNil, indexBy, prop} from "ramda"
const conf = (await db.get("conf", "mirror-calc")) || { ver: 0 }
const exists = (await db.get("mirror", ["ver"], ["ver", "!=", 0])) || []
let exists_map = indexBy(prop("id"))(exists)
const day = 60 * 60 * 24
const two_weeks = day * 14
const now = Date.now() / 1000
const deadline = now - two_weeks
const bookmarks = await db.get(
"bookmarks",
["date", "desc"],
["date", ">=", deadline]
)
const rank = {}
let batches = [
["upsert", { ver: conf.ver + 1, date: now }, "conf", "mirror-calc"],
]
for (let v of bookmarks) {
if (isNil(rank[v.article_id])) {
rank[v.article_id] = {
id: v.article_id,
pt: 0,
bookmarks: 0,
}
}
rank[v.article_id].bookmarks += 1
const k = (two_weeks - (now - v.date)) / day
rank[v.article_id].pt += k
}
for (let k in rank) {
let v = rank[k]
if (!isNil(exists_map[k])) {
exists_map[k].exists = true
}
batches.push([
"upsert",
{
id: k,
ver: conf.ver + 1,
pt: v.pt,
bookmarks: v.bookmarks,
},
"mirror",
k,
])
}
for (let k in exists_map) {
if (exists_map[k].exists !== true) {
batches.push(["update", { pt: db.del(), ver: db.del() }, "mirror", k])
}
}
await db.batch(batches)
Advanced: Calculate Bookmark Counts with Cron
You can do the same thing by automating the periodical calculation with a cron.
const cron = {
span: 60 * 60 * 12, // execute every 12 hours
do: true,
jobs: [
["get", "conf", ["conf", "mirror-calc"], { ver: 0 }],
["get", "exists", ["mirror", ["ver"], ["ver", "!=", 0]]],
["let", "exists_map", ["indexBy", ["prop", "id"], { var: "exists" }]],
["let", "day", 60 * 60 * 24],
["let", "two_weeks", ["multiply", { var: "day" }, 14]],
["let", "now", { var: "block.timestamp" }],
["let", "deadline", ["subtract", { var: "now" }, { var: "two_weeks" }]],
[
"get",
"bookmarks",
["bookmarks", ["date", "desc"], ["date", ">=", { var: "deadline" }]],
],
["let", "rank", {}],
[
"let",
"batches",
[
[
"upsert",
{
ver: ["add", 1, ["prop", "ver", { var: "conf" }]],
date: { var: "now" },
},
"conf",
"mirror-calc",
],
],
],
[
"do",
[
"forEach",
[
"pipe",
["let", "v"],
["prop", "article_id"],
["pair", "rank"],
["join", "."],
["let", "rank_path"],
[
"when",
["pipe", ["var", "$rank_path"], ["isNil"]],
[
"pipe",
[
"applySpec",
{
id: ["identity"],
pt: ["always", 0],
bookmarks: ["always", 0],
},
],
["let", "$rank_path"],
],
],
["var", "$rank_path"],
["over", ["lensProp", "bookmarks"], ["inc"]],
["let", "$rank_path"],
["var", "v"],
["prop", "date"],
["subtract", { var: "now" }],
["subtract", { var: "two_weeks" }],
["divide", ["__"], { var: "day" }],
["let", "k"],
["var", "$rank_path"],
[
"over",
["lensProp", "pt"],
[
"pipe",
["applySpec", { pt: ["identity"], k: ["var", "k"] }],
["values"],
["sum"],
],
],
["let", "$rank_path"],
],
{ var: "bookmarks" },
],
],
[
"do",
[
"forEachObjIndexed",
[
"pipe",
["unapply", ["take", 2]],
["tap", ["pipe", ["head"], ["let", "v"]]],
["pipe", ["last"], ["let", "k"]],
["pair", "exists_map"],
["join", "."],
["let", "ex_path"],
["var", ["__"], true],
[
"when",
["pipe", ["isNil"], ["not"]],
["pipe", ["assoc", true, "exists"], ["let", "$ex_path"]],
],
["var", "v"],
[
"applySpec",
{
method: ["always", "upsert"],
query: {
id: ["var", "k"],
ver: ["pipe", ["var", "conf"], ["prop", "ver"], ["inc"]],
pt: ["prop", "pt"],
bookmarks: ["prop", "bookmarks"],
},
collection: ["always", "mirror"],
doc: ["var", "k"],
},
],
["values"],
["applySpec", { query: ["identity"], batches: ["var", "batches"] }],
["values"],
["apply", ["append"]],
["let", "batches"],
],
{ var: "rank" },
],
],
[
"do",
[
"forEachObjIndexed",
[
"pipe",
["unapply", ["take", 2]],
["tap", ["pipe", ["head"], ["let", "v"]]],
["pipe", ["last"], ["let", "k"]],
],
["pair", "exists_map"],
["join", "."],
["var", ["__"], true],
[
"when",
["pipe", ["propEq", "exists", true], ["not"]],
[
"pipe",
[
"applySpec",
{
method: ["always", "update"],
query: {
pt: ["always", db.del()],
ver: ["always", db.del()],
},
collection: ["always", "mirror"],
doc: ["var", "k"],
},
],
["values"],
["applySpec", { query: ["identity"], batches: ["var", "batches"] }],
["values"],
["apply", ["append"]],
["let", "batches"],
],
],
],
],
["batch", { var: "batches" }],
],
}
await db.addCron(cron, "count")
Get Trending Articles
The following query will fetch top 10 trending articles in the past 2 weeks.
const articles = await db.get("mirror", ["pt", "desc"], 10)
The Frontend Dapp
The frontend implementation will be omitted for this tutorial.
An example dapp with the same setup as this tutorial is deployed at asteroid.ac.