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 → introspectworkflow, 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.