ssb-crut
Easily mint CRUD (Create, Read, Update, Delete) methods for scuttlebutt records!
Note there is no Delete, so instead we have T for Tombstone (CRUT!)
Example usage
const Crut = require('ssb-crut')
const Overwrite = require('@tangle/overwrite')
const SimpleSet = require('@tangle/simple-set')
const spec = {
type: 'gathering',
props: {
title: Overwrite(),
description: Overwrite(),
attendees: SimpleSet()
}
}
const crut = new Crut(ssb, spec)
crut.create(
{
title: 'Ahau launch party',
attendees: { add: ['Mix'] },
recps: ['%A9OUzXtv7BhaAfSMqBzOO6JC8kvwmZWGVxHDAlM+/so=.cloaked']
},
(err, gatheringId) => {
}
)
crut.update(
gatheringId,
{
description: "Let's celebrate this new phase!",
attendees: { add: ['Cherese'] }
},
(err, updateId) => {
crut.read(gatheringId, (err, gathering) => {
})
}
)
Requirements
You require an ssb instance with some index plugins installed
db1 plugins:
const stack = require('secret-stack')({ caps })
.use(require('ssb-db'))
.use(require('ssb-backlinks'))
const ssb = stack(opts)
db2 plugins:
const stack = require('secret-stack')({ caps })
.use(require('ssb-db2'))
.use(require('ssb-db2/compat/db'))
.use(require('ssb-db2/compat/history-stream'))
.use(require('ssb-db2/compat/feedstate'))
const ssb = stack(opts)
Optionally, if you want to make private (encrypted) records, you will need to have a plugin installed
which knows how to handle your recps
e.g.
ssb-tribes
(recommended) for records stored in a private group, or direct messagesssb-private1
for records as direct messages
API
new CRUT(ssb, spec, opts?) => crut
Takes ssb
, a scuttlebutt server instance, a spec
and an optional
opts and returns an crut instance with methods for mutable records.
A spec
is an Object with properties:
spec.type
String - directly related to the type
field that will occur on messagesspec.props
Object
- defines how the mutable parts of the record will behaved.
- each property is expected to be an instance of a tangle strategy (e.g.
@tangle/simple-set
) - reserved props:
['type', 'recps', 'tangle', 'tombstone']
Optional properties:
opts
can have the following properties:
publish(content, cb)
, a custom publish function instead of the default ssb.publish. Could be publishAs
when using ssb-db2 to publish the content as another feedIdfeedId
, the feedId to publish as. Defaults to ssb.id.
crut.create(allProps, cb)
Makes a new record, and calls back with the id
of that record.
allProps
Object
- none/ some/ all of the properties declared in
spec.props
- none/ some/ all of the properties declared in
spec.staticProps
recps
Array (optional) a list of recipients who this record will be encrypted to. Once this is set on create, it cannot be updatedallowPublic
Boolean (optional) for if you have ssb-guard-recps
installed and want to explicitly allow a public message through
Notes:
- if
cb
is not passed, a Promise is returned instead.
crut.read(id, cb)
Takes a record id and calls back with a Record.
A tangle here is a collection of messages linked in a directed acyclic graph.
Each of thee messages contains some transformation(s) which are an instuction about how to update the record state.
Transformations are concatenated (added up) while traversing the graph.
For a tangle made up of messages linked like this:
A << root
/ \
B C << concurrent updates
|
D << an update which is also a tip
Then the reduced Record would look like:
{
key: A,
type,
...staticProps,
...props
states: [
{
key: D,
...props
},
{
key: B,
...props
}
},
conflictFields: []
}
There will be 1 or more "states" depending on whether the tangle is a in a
branched / forked state at the moment.
For convenience the states are automatically merged and spread into the result.
If the states are in conflict then the first state is used as a 'best guess'
The state of the props returned are "riefied" (meaning has been made real),
because often the transformation format is optimised for mathematical properties,
but not very human friendly.
Notes:
states
is sorted "most recent" to "least recent" by the tip messages's declared timestamp.- if
cb
is not passed, a Promise is returned instead.
crut.update(id, props, cb)
Updates record id
.
props
Object can have properties
- all/ some/ none of the props declared in
spec.props
allowPublic
Boolean (optional) for if you have ssb-guard-recps
installed and want to explicitly allow a public message through
The props
provided are used to generate a transformation which is then checked with isValidUpdate
(if provided), they are:
Message contents are also checked against isUpdate
before publishing.
Calls back with the key of the update message published.
Notes:
-
if cb
is not passed, a Promise is returned instead.
-
if there is a merge conflict that needs resolving and your update does not resolve it you will get an Error with
err.message
- describes in human sentence the fields which has conflictserr.fields
- an Array of fields which had conflicts
-
by default, updates are accepted from everyone. To change this, specifiy behaviour in isValidUpdate
e.g.
spec.isValidUpdate = (context, msg) => {
const { accT, graph } = context
if (!graph) return true
return graph.rootNodes.some(root => {
return root.value.author === msg.value.author
})
}
crut.tombstone(id, opts, cb)
A convenience helper mainly here to put the T in CRUT.
opts
Object with properties:
reason
String (optional) give a reason for why you're tombstoning the recordundo
Boolean (optional) set to true
to remove the tombstoneallowPublic
Boolean (optional) for if you have ssb-guard-recps
installed and want to explicitly allow a public message through
Calls back with the key of the update message which tombstoned.
Notes:
- if
cb
is not passed, a Promise is returned instead.
Using spec.arbitraryRoot with Private Groups
If you set spec.arbitraryRoot
to true, then you can use a groupId
that you're a part of and ssb-crut
will automatically root your record at the group init message.
To use this feature you must be using ssb-tribes >= 2.7.0
The methods you can do this with are:
crut.updateGroup(groupId, props, cb)
crut.readGroup(groupId, cb)
crut.tombstoneGroup(groupId, opts, cb)
Reminder there is no crut.create
when you have an arbitraryRoot
TODO
- Show user error message caused by failed merge