Tutorial

This tutorial walks through a complete example from Node.js: loading a data model, seeding a database, and performing a full write / read / history cycle with CommitDatabase. It is the Node twin of the Python Tutorial.

Prerequisites: This tutorial assumes you have read DSM Processing and understand the assemble parse introspect workflow, plus the Database lifecycle (stage → commit → reconstruct).

const {
  DSMBuilder, DefinitionsInspector,
  CommitDatabase, CommitMutableState, CommitStateBuilder, Path,
} = require('@digitalsubstrate/dsviper');

The User/Login Model

We’ll use a simple model with Users who have Login credentials and Identity information.

Step 1: Define the Data Model

Create a file model.dsm:

// Types definitions
namespace Tuto {f529bc42-0618-4f54-a3fb-d55f95c5ad03} {

"""A user."""
concept User;

"""Login credentials for a User."""
struct Login {
    string nickname;
    string password;
};

"""Identity information for a User."""
struct Identity {
    string firstname;
    string lastname;
};

"""A small avatar image stored inline."""
struct Thumbnail {
    uint16 width;
    uint16 height;
    blob data;
};

"""A high-resolution texture stored by reference."""
struct Texture {
    uint16 width;
    uint16 height;
    blob_id pixels;
};

"""Lifecycle state of a User account."""
enum Status {
    pending,
    active,
    completed
};

"""Account state for a User."""
struct Account {
    Status state;
};

attachment<User, Login> login;
attachment<User, Identity> identity;
attachment<User, Thumbnail> avatar;
attachment<User, Texture> portrait;
attachment<User, Account> account;

};

Step 2: Validate the Model

Parsing both validates the syntax and hands back the runtime definitions. Always check the report before trusting the result:

const [report, dsmDefs, defs] = DSMBuilder.assemble('model.dsm').parse();
report.hasError(); // false

defs is a DefinitionsConst — the immutable runtime snapshot, ready for a database to adopt.

Step 3: Create a Database

Unlike the Python flow — which builds a .cdb file up front with dsm_util — the Node lifecycle creates the store in process and extends it with the definitions you just parsed:

const db = CommitDatabase.createInMemory();
db.extendDefinitions(defs);
defs.isEqual(db.definitions()); // true

Step 4: Open and Explore

A DefinitionsInspector lists the types and attachments the database carries:

const insp = new DefinitionsInspector(defs);

insp.conceptTypeNames().map(String);     // ['Tuto::User']
insp.enumerationTypeNames().map(String); // ['Tuto::Status']
insp.structureTypeNames().map(String);
// ['Tuto::Account', 'Tuto::Identity', 'Tuto::Login', 'Tuto::Texture', 'Tuto::Thumbnail']

insp.attachmentIdentifiers();
// ['Tuto::User.account', 'Tuto::User.avatar', 'Tuto::User.identity',
//  'Tuto::User.login', 'Tuto::User.portrait']

Step 5: Look Up the Attachment

Note

No constant injection. The Python API can inject() types as constants into the caller’s namespace (TUTO_A_USER_LOGIN). Node has no such global injection: you look a type or attachment up explicitly, by its identifier, from the inspector. The identifier strings are exactly the ones attachmentIdentifiers() returns.

const login_att = insp.queryAttachment('Tuto::User.login');
login_att.keyType().typeName().name();      // 'User'
login_att.documentType().typeName().name(); // 'Login'

query* returns undefined on a miss; use check* (e.g. insp.checkAttachment) when you want a ViperError thrown instead.

Step 6: Create a Key and Document

Create a key for a new User. instanceId() returns the underlying UUID (randomly generated):

const key = login_att.createKey();
key.instanceId().isValid(); // true

Create a Login document and fill its fields. Structures have no attribute protocol — read with .at(), write with .assign() (a batch setter that returns the structure) or .set(field, value) for a single field:

const login = login_att.createStructure({ nickname: '', password: '' });
login.at('nickname'); // ''

login.assign({ nickname: 'zoop', password: 'robust' });
login.at('nickname'); // 'zoop'

Step 7: Commit to Database

Stage the document against the key on a mutable state, then seal the batch into a commit:

const state = new CommitMutableState(CommitStateBuilder.initialState(db));
state.attachmentMutating().set(login_att, key, login);

const commitId = db.commitMutations('First Commit', state);
String(commitId).length;          // 40  (a ValueCommitId)
db.lastCommitId().equals(commitId); // true

Note

Scope of this tutorial

The single-author flow shown here does not exercise the reduction behaviour described in Commit: with one author committing in sequence, mutations are never silently dropped and no last-writer-wins arbitration takes place. The contract becomes load-bearing once several authors’ writes are reduced automatically — see Modes of Use for the diagnostic and which mode applies to your application.

Step 8: Read from Database

A read is a reconstruction: CommitStateBuilder.state replays the mutation trace up to a commit and hands you an immutable snapshot. get returns a ValueOptional.unwrap() peels it to the document:

const s = CommitStateBuilder.state(db, commitId);
const result = s.attachmentGetting().get(login_att, key);

result.isNil();             // false
result.unwrap().at('nickname'); // 'zoop'

Value semantics are Java-like: compare documents with .equals(), never with == between two Values (that is reference equality).

Step 9: Update a Field

Update one location through a Path rather than re-setting the whole document. Capture the new commit id that commitMutations returns — reads always address an explicit commit, so chaining requires the id:

const next = new CommitMutableState(CommitStateBuilder.state(db, commitId));
next.attachmentMutating().update(login_att, key, new Path().field('nickname').const(), 'zoopy');

const updatedId = db.commitMutations('Update Nickname', next);

Step 10: View History

Each commit is a point in the history; reconstruct the state at any of them:

CommitStateBuilder.state(db, updatedId)
  .attachmentGetting().get(login_att, key).unwrap().at('nickname'); // 'zoopy'

CommitStateBuilder.state(db, commitId)
  .attachmentGetting().get(login_att, key).unwrap().at('nickname'); // 'zoop'

// Before any write, the document is absent:
CommitStateBuilder.initialState(db)
  .attachmentGetting().get(login_att, key).isNil(); // true

Step 11: Inspect Commit Headers

const header = db.commitHeader(updatedId);
header.label();                          // 'Update Nickname'
header.parentCommitId().equals(commitId); // true

The walkthrough above uses the dynamic API (definitions loaded at runtime). The same operations are available through the static API — a typed TypeScript package generated from the model by Kibo, catalogued under Templated Features. See Value Chains for the contrast.