# ssb-crut

> easy CRUT methods for secure scuttlebutt

Latest version **6.1.1** (published 2024-04-22) · LGPL-3.0-only license · 0 weekly downloads

## Install

```sh
npm install ssb-crut
pnpm add ssb-crut
yarn add ssb-crut
bun add ssb-crut
```

## Health

**Score 15/100 (F)** — status: abandoned.

Positive: no vulnerabilities.

Warnings: low downloads; no types; no esm support.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 6.1.1 |
| Published | 2024-04-22 |
| First published | 2020-11-05 |
| Weekly downloads | 0 |
| License | LGPL-3.0-only |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 9 |
| Unpacked size | 136 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | mixmix |
| Maintainers | powersource, ben-tai, chereseeriepa, mixmix, arj03 |
| Keywords | scuttlebutt, CRUT, records |

## Links

- npm: https://www.npmjs.com/package/ssb-crut
- Repository: https://gitlab.com/ahau/ssb-crut
- Homepage: https://gitlab.com/ahau/ssb-crut#readme
- Issues: https://gitlab.com/ahau/ssb-crut/issues
- npm.io page: https://npm.io/package/ssb-crut

## Dependencies (9)

- [pull-stream](https://npm.io/package/pull-stream.md) ^3.7.0
- [lodash.merge](https://npm.io/package/lodash.merge.md) ^4.6.2
- [pull-paramap](https://npm.io/package/pull-paramap.md) ^1.2.2
- [@tangle/graph](https://npm.io/package/@tangle/graph.md) ^3.2.0
- [@tangle/reduce](https://npm.io/package/@tangle/reduce.md) ^5.0.5
- [is-my-ssb-valid](https://npm.io/package/is-my-ssb-valid.md) ^1.2.2
- [@tangle/strategy](https://npm.io/package/@tangle/strategy.md) ^4.1.2
- [ssb-schema-definitions](https://npm.io/package/ssb-schema-definitions.md) ^3.2.1
- [fast-json-stable-stringify](https://npm.io/package/fast-json-stable-stringify.md) ^2.1.0

## Recent versions

- 6.1.1 (latest) — 2024-04-22
- 6.1.0 — 2023-12-12
- 6.0.0 — 2023-12-10
- 4.6.4 — 2023-10-30
- 5.1.1 — 2023-10-30
- 5.1.0 — 2023-04-27
- 5.0.0 — 2023-04-17
- 4.6.3 — 2023-04-16
- 4.6.2 — 2023-03-24
- 4.6.1 — 2022-09-23
- 4.6.0 — 2022-07-02
- 4.5.2 — 2022-06-27
- 4.5.1 — 2022-06-13
- 4.5.0 — 2022-06-02
- 4.4.2 — 2022-05-29
- … 23 more at https://npm.io/package/ssb-crut/versions

## README

# 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

```js
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) // ssb = an ssb server

crut.create(
  {
    title: 'Ahau launch party',
    attendees: { add: ['Mix'] },

    recps: ['%A9OUzXtv7BhaAfSMqBzOO6JC8kvwmZWGVxHDAlM+/so=.cloaked']
    // makes it a private record, can only be set on create
  },
  (err, gatheringId) => {
    //
  }
)
```

```js
// later

crut.update(
  gatheringId,
  {
    description: "Let's celebrate this new phase!",
    attendees: { add: ['Cherese'] }  // props
  },
  (err, updateId) => {
    crut.read(gatheringId, (err, gathering) => {
      //
    })
  }
)
```

## Requirements

You require an ssb instance with some index plugins installed

**db1 plugins:**
```js
const stack = require('secret-stack')({ caps })
  .use(require('ssb-db'))
  .use(require('ssb-backlinks'))
  .use(require('ssb-query'))
  // ...

const ssb = stack(opts)
```

**db2 plugins:**
```js
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 messages
- `ssb-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 messages
- `spec.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 keys: `allowPublic, conflictFields, key, originalAuthor, reason, recps, states, tangles, tombstone, type, undo`

Optional properties:
- `spec.tangle` *String*
    - where the tangle info will be stored `content.tangles[tangle]`
    - defaults to `type.split('/')[0]`
- `spec.typePattern` *String*
    - a regex pattern string which can be used to over-write the default. Useful if you've used regex characters that need escaping in you `spec.type` (e.g. `*`)
    - defaults to : `"^" + spec.type + "$"`
- `spec.staticProps` *Object*
    - a list of props which are not mutable, and will be baked into root message
    - format is in JSON Schema, for example:
    ```
    {
      source: { type: 'string', required: true },
      live: { type: 'boolean' }
    }
    ```
    - reserved keys: `allowPublic, conflictFields, key, originalAuthor, reason, recps, states, tangles, tombstone, type, undo`
- `spec.nextStepData` *Object*
    - a list of props used only in a transformation, and thus will not show up in final state
    - can be accessed in `isValidNextStep` under `node.data`
    - format is in JSON Schema, for example:
    ```
    {
      source: { type: 'string' },
      live: { type: 'boolean' }
    }
    ```
    - reserved keys: `allowPublic, conflictFields, key, originalAuthor, reason, recps, states, tangles, tombstone, type, undo`
    - must not include any already defined in either `props` or `staticProps`
- `spec.isValidNextStep` *Function*
    - a function run before writing and during reading to confirm the validity of message extending the tangle
    - signature: `fn(tangleContext, node, ssb) => Boolean` where

        - `tangleContext = { tips, graph, ssb }` where:
            - `tips` *Array* represents the accumulated transform for the position immediately before this message in the tangle, `[{ key, T }]`
            - `graph` is a `@tangle/graph` instance for the tangle so far.
        - `node` the node being assessed is of form:
          ```js
          {
            key: MessageId,
            previous: ['%sd4kasDD...'],
            data: {
              title: { set: 'spec gathering' },
              attendees: { mix: 1, luandro: 1 }
            },
            author: '@ye+4das...',
            sequence: 132
          }
          ```
        - `ssb` the ssb server
    - used by:
        - `create` to check the message about to be published is valid. In this case `accT = I`, the (empty) identity transform.
        - `update` to check the message about to be published is valid given the existing tangle state it would extend
        - `read` to determine which messages are valid to include in reducing
    - NOTE - in `create` you don't have access to `tangleContext.graph`, as there's no graph yet
    - ADVANCED - if you want to provide a detailed error, you can attach errors to your validator like so:
         ```js
         function isValidNextStep (tangleContext, node) {
           isValidNextStep.error = null

           const isValid = ...  // your logic
           if (isValid) return true
           else {
             isValidNextStep.error = new Error('your detailed error message')
             return false
           }
         }
         ```
- `spec.hooks` *Object* with properties:
    - `isRoot` *Array* a collection of validator functions which each root message must pass to proceed
    - `isUpdate` *Array* a collection of validator functions which each update message must pass to proceed
    - NOTE these validators are expected to have signature `isValid (content) => true|Error`
- `spec.getTransformation` *Function*
    - a function of signature `(msg, d)` used to map a full message to a transformation
       - `msg` is a message in the form `{ key, value }`
       - `d` is an index of the message within a flattened version of the tangle
    - default: `(msg) => msg.value.content`
    - this is particularly useful for coercing legacy messages into a format your spec can process
    - WARNING: **create a new object** if you plan to mutate the shape of a message, otherwise ssb-crut will break
- `spec.arbitraryRoot` *Boolean*
    - default: `false`
    - if `true`, then the tangle root of the record can be any messageId
    - this allows you to attach (for example) a `settings` record to a group root message
    - disables `crut.create` (as you already have your root created), so you start with `crut.update`
    - during tangle reducing, an empty root node with the right shape is fabricated

`opts` can have the following properties:
- `opts.create(input, cb)`, a custom publish function which is expected where:
    - `input` is `{ content, allowPublic }`
       - `content` is the message content
       - `allowPublic` will be present and `true` if `allowPublic` was passed into `create/update`. It's there to be used with `ssb-recps-guard`
- `opts.feedId`, the feedId to publish as. Defaults to ssb.id.
- `opts.alwaysIncludeStates` *Boolean* (default: `false`)
    - the current version only includes head state of the tangle IF there are > 1 head to the tangle AND there is a conflict between those heads
    - setting this to `true` will ensure that `record.states` is always populated

### `crut.create(input, cb)`

Makes a new record, and calls back with the `id` of that record.

- `input` *Object* where key/value can be
  - all/some/none declared in `spec.props`
  - all/some/none declared in `spec.staticProps`
  - all/some/none declared in `spec.nextStepData`
  - `recps` *Array* (optional) a list of _recipients_ who this record will be encrypted to. Once this is set on create, it cannot be updated
  - `allowPublic` *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:

```js
{
  key: A,                  // the key of the tangle root message
  type,
  ...staticProps,          // any staticProp values
  ...props,                // best guess of state (auto-merged states || most recent state)
  conflictFields: ['name'] // IF conflict, names trouble fields
  states: [                // IF conflect shows the full state of tips
    {
      key: D,              // key of tangle tip message
      name: 'Mix'          // reified state of props for this tangle tip
      // ...                  
    },
    {
      key: B,
      name: 'mixmix',
      // ...
    }
  },
}
```

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, input, cb)`

Updates record `id`.

- `input` *Object* containing key/values:
  - all/some/none declared in `spec.props`
  - all/some/none declared in `spec.nextStepData`
  - `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 conflicts
    - `err.conflictFields` - an Array of fields which had conflicts

- by default, **updates are accepted from everyone**. To change this, specify behaviour in `isValidUpdate` e.g.
    ```js
    spec.isValidUpdate = (context, msg) => {
      const { accT, graph } = context

      if (!graph) return true
      // crut.read has graph, but crut.update doesn't yet
      // this means updates from others can be published but will be ignored

      return graph.rootNodes.some(root => {
        return root.value.author === msg.value.author
      })
    }
    ```
    - see also `ssb-crut-authors`


### `crut.tombstone(id, input, cb)`

A convenience helper mainly here to put the T in CRU**T**.

- `input` *Object* with properties:
  - `reason` *String* (optional) give a reason for why you're tombstoning the record
  - `undo` *Boolean* (optional) set to `true` to remove the tombstone
  - all/some/none keys declared in `spec.nextStepData`
  - `allowPublic` *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.


### crut.list(opts, cb)

List all records (or some subset) of this crut type from your database.

`opts` *Object* (optional) where:
- `opts.limit` *Integer* - how many records you would like (after all filters have been applied)
- `opts.filter` *Function* - apply a filter to records being returned (is given a full `record`)
- `opts.groupId` *GroupId* (a String)
    - a convenience filter which only allows records published to a particular group
    - NOTE - not yet available for `ssb-db2`
- `opts.orderBy` *String*
    - define what order to pull the records in by, looks at the times recorded on the **initial message** of the record.
    - options: 
         - `receiveTime` = order by when the record was first received/ written (default)
         - `updateTime` = order by how recently writes (create/update) to the record were received
         - `createTime` = order by when the record was created (as asserted by the author)
- `opts.descending` *Boolean*
    - sets whether the receiveTime/createTime/updateTime is _descending_ throughour the results
    - default: true
- `opts.startFrom` *String*
    - start reading from a particular message key _onwards_ (it will exclude the provided key)
- `opts.tombstoned` *Boolean* (default: false)
    - only show tombstoned records
- `opts.read` *Function*
    - provide a custom 'read' function which allows e.g. decorating, or installing a cache
    - default:
        ```js
        (id, cb) => {
          crut.read(id, (err, record) => {
            if (err) cb(null, null) // prevents one failed read from causing whole like to fail
            else cb(null, record)
          })
        }
        ```
        - where `recordId` is the id of the root message of the record tangle
- `opts.width` *Integer*
    - how many `read` functions to run in parallel
    - default: `5`

---

## 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, input, cb)`
- `crut.readGroup(groupId, cb)`
- `crut.tombstoneGroup(groupId, input, cb)`

Reminder there is no `crut.create` when you have an `arbitraryRoot`

---
_Source: https://npm.io/package/ssb-crut · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
