TS ORM SDK

Reactive ORM library for NextGraph: use reactive (typed) objects that automatically sync to NextGraph’s encrypted, local-first storage.

For a walk-through you can see the the expense-tracker example apps for discrete JSON documents or typed graph documents.

Why?

Different CRDTs have different APIs. We want to make it as easy as possible to use them in the same way:
You modify a plain old TypeScript object and that updates the CRDT.
Vice versa, the CRDT is modified and that is reflected in your TS object.
We offer this for React, Vue, and Svelte (5 and 4).

Note that we support discrete (JSON) CRDT and graph (RDF) CRDT ORMs.

  • For graphs, you specify a schema using a SHEX shape and optionally a scope. This provides you with typing support.
  • For discrete CRDTs, all you need is a document ID (NURI).

Table of Contents


Installation

pnpm add @ng-org/orm @ng-org/web

For schema generation, also install:

pnpm add -D @ng-org/shex-orm

Start

Before writing your own app, you are strongly advised to look at the example apps below, where you can find framework and crdt-specific walkthroughs.

The app looks the same in all implementations. You can see that the useShape() and useDiscrete() frontend hooks that interact with the data, share the same syntax across all frameworks.


Before using the ORM, initialize NextGraph in your app entry point:

import { ng, init } from "@ng-org/web";
import { initNg } from "@ng-org/orm";

// Call init as early as possible when your app loads.
// At the first call, it will redirect the user to login with their wallet.
// In that case, there is no need to render the rest of the app.
// Then your app will reload, and this time, this call back will be called:
await init(
  async (event) => {
    // The ORM needs to have access to ng,
    // the interface to the engine running in WASM.
    initNg(ng, event.session);
  },
  true,
  [],
);

RDF (graph) ORM: Defining Schemas

Define your data model using SHEX (Shape Expressions): See @ng-org/shex-orm for details.

shapes/shex/dogShape.shex:

PREFIX ex: <did:ng:z:>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

ex:Dog {
    a [ex:Dog] ;
    ex:name xsd:string ;
    ex:age xsd:integer ? ;
    ex:toys xsd:string * ;
    ex:owner IRI ? ;
}

Add the following to your package.json scripts and run build:orm (assuming you installed @ng-org/shex-orm as dev dependency):

"build:orm": "rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm"

This will generate three files: one for TypeScript type definitions, one with generated schemas, and one that exports objects with the schema, type definition and shape name: The so called shape types. When you request data from the engine, you will pass a shape type in your request that defines what your object looks like.

Frontend Framework Usage

The SDK offers hooks for discrete and graph-based CRDTs for Svelte, Vue and React:

All of them share the same logic. They create a 2-way binding to the engine. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT and the components refresh. When the component unmounts, the subscription is closed.

// Queries the graphs with NURI did:ng:o:g1 and did:ng:o:g2 and with subject s1 or s2.
const expenses = useShape(ExpenseShapeType, {
  graphs: ["did:ng:o:g1", "did:ng:o:g2"],
  subjects: ["<s1 IRI>", "<s2 IRI>"],
});
// Note: While the returned `expenses` object has type `DeepSignal<Set<Expense>>`, you can treat and type it as `Set<Expense>` as well, for convenience.

// Now you can use expenses in your component
// and modify them to trigger a refresh and persist them.

// In analogy:
const expense: DeepSignal<Expense[]> = useDiscrete(expenseDocumentNuri);
// Note: While the returned `expenses` object has type `DeepSignal<Expense[]>`, you can treat and type it as `Expense[]` as well, for convenience.

Working with Data

The ORM is designed to make working with data as normal as possible. You get an object as you are used to it and when you change properties, they are automatically persisted and synced with other devices. Conversely, modifications coming from other devices update the ORM objects too and your components refresh.

Creating a Document

First, you need a document to store and get your data. With the document NURI, you can then create ORM objects.

// Create a new NextGraph document
const docNuri = await ng.doc_create(
  session_id,
  "Graph", // Or "YMap" or "Automerge", for discrete
  "data:graph", // Or "data:json" : "data:map" for Automerge or YJs
  "store",
  undefined,
);

// Add class to RDF part of the document so we can find it again.
await ng.sparql_update(
  session_id,
  `INSERT DATA { GRAPH <${documentId}> {<${documentId}> a <${APPLICATION_CLASS_IRI}> } }`,
  documentId,
);

To find your document NURI, you make a sparql query:

const ret = await ng.sparql_query(
  session_id,
  `SELECT ?storeId WHERE { GRAPH ?storeId { ?s a <${APPLICATION_CLASS_IRI}> } }`,
  undefined,
  undefined,
);
let documentId = ret?.results.bindings?.[0]?.storeId?.value;

Using and Modifying ORM Objects

There are multiple ways to get and modify data:

  • Get and modify the signalObject of the subscription returned by Orm(Discrete)Subscription.getOrCreate().
  • Get and modify the data returned by a useShape() or useDiscrete() hook inside of a component.
  • For graph ORMs (no 2-way binding):
    • getObjects(shapeType, scope) Gets all object with the given shape type within the scope. The returned objects are not DeepSignal objects - modifications to them do not trigger updates and changes from other sources do not update the returned object.
    • insertObject(shapeType, object): A convenience function to add objects of a given shape to the database. While with useShape() and OrmSubscription, you can just add objects to the returned set or subscription.signalObject, respectively. This function spares you of creating an OrmSubscription and can be used outside of components, where you can’t call useShape.

The (Discrete)OrmSubscription Class

You can establish subscriptions outside of frontend components using the (Discrete)OrmSubscription class. DiscreteOrmSubscriptions are scoped to one document, (RDF-based) OrmSubscriptions can have a Scope of more than one document and require a shape type. Once a subscription is established, its .readyPromise resolves and the .signalObject contains the 2-way bound data.

You can create a new subscription using (Discrete)OrmSubscription.getOrCreate(). If a subscription with the same document or scope exists already, a reference to that object is returned. Otherwise, a new one is created. This pooling is especially useful when more than one frontend component subscribes to the same data and scope by calling useShape() or useDiscrete(). This reduces load and the data is available instantly.

Subscriptions are open until .close() is called on all references of this object. The useShape and useDiscrete hooks call .close() on their reference when their component unmounts.

Transactions

To improve performance, you can start transactions with subscriptions using .beginTransaction() and .commitTransaction(). This will delay the persistence until .commitTransaction() is called. Transactions do not affect updates to the frontend and incoming updates from the engine / other devices. When more than one reference to a subscription exists, the transaction affects all of them.

Note that even in non-transaction mode, changes are batched and only committed after the current task finished. The changes are sent to the engine in a microtask. You can end the current task and flush, for example, by awaiting a promise: await Promise.resolve().

Note that you can use the signal object of an orm subscription (e.g. myOrmSubscription.signalObject) in components too. For that, you need to use useDeepSignal(signalObject) from the package @ng-org/alien-deepsignals/svelte|vue|react. This can be useful to keep a connection open over the lifetime of a component and to avoid the loading time when creating new subscriptions.

Example of using an OrmSubscription:

const dogSubscription = OrmSubscription.getOrCreate(DogShape, {
  graphs: [docNuri],
});
await dogSubscription.readyPromise;

// If we used OrmDiscreteSubscription, the signalObject type would be an array or object.
const dogSet: DeepSignal<Set<Dog>> = dogSubscription.signalObject;

dogs.add({
  // Required: The document NURI. May be set to `""` for nested objects (will be inherited from parent object then).
  "@graph": docNuri,
  "@type": "did:ng:x:Dog", // Required: RDF type
  "@id": "", // Empty string = auto-generate subject IRI
  name: "Mr Puppy",
  age: 2,
  toys: new Set(["ball", "rope"]),
});

// When you know that only one element is in the set, you can call `.first()` to get it.
const aDog = dogs.first();
aDog.age += 1;
aDog.toy.add("bone");

// Utility to find objects in sets:
const sameDog = dogs.getBy(aDog["@graph"], aDog["@id"]);
// sameDog === aDog.

dogs.delete(aDog);

Note that the RDF CRDT supports sets only, the discrete CRDTs arrays only.

The DeepSignal<> type

Data returned by the ORM is of type DeepSignal<T>. It behaves like plain objects of type T but with some extras. Under the hood, the object is proxied. The proxy tracks modifications and will immediately update the frontend and propagate to the engine.

In your code however, you do not have to to wrap your type definitions in DeepSignal<>. Nevertheless, it can be instructive for TypeScript to show you the additional utilities that DeepSignal objects expose. Also, it might keep you aware that modifications you make to those objects are persisted and that they update the frontend. The utilities that DeepSignal objects include are:

  • For sets (with the RDF ORM):
    • iterator helper methods (e.g. map(), filter(), reduce(), any(), …)
    • first() to get one element from the set — useful if you know that there is only one.
    • getBy(graphNuri: string, subjectIri: string), to find objects by their graph NURI and subject IRI.
    • NOTE: When assigning a set to DeepSignal<Set>, TypeScript will warn you. You can safely ignore this by writing (parent.children = new Set() as DeepSignal<Set<any>>). Internally, the set is automatically converted but this is not expressible in TypeScript.
  • For all objects: __raw__ which gives you the non-proxied object without tracking value access and without triggering updates upon modifications. Tracking value access is used in the frontend so it knows on what changes to refresh. If you use __raw__, that won’t work anymore.

Graph ORM: Relationships

To reference external objects, you can use their @id.

casey.friends.add(jackNuri);

// When the child object is a nested object that you do not have in memory,
// you can establish the link by adding an object that contains the `@id` property only.
shoppingExpense.category.add({ "@id": "<Subject IRI of expense category>" });

// Link objects by storing the target's `@id` NURI/IRI:
dog.owner = jackNuri;

// Resolve the relationship
const jack = people.find((p) => p["@id"] === dog.owner);

Note that when you delete a nested object from a parent, only the linkage to it is removed. The nested object itself (its quads) are not deleted.


Reference

Classes

DiscreteOrmSubscription

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:44

Class for managing RDF-based ORM subscriptions with the engine.

You have two options on how to interact with the ORM:

  • Use a hook for your favorite framework under @ng-org/orm/react|vue|svelte
  • Call OrmSubscription.getOrCreate to create a subscription manually

For more information about RDF-based ORM subscriptions, follow the tutorial at the top of this file.

Properties

documentId

readonly documentId: string

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:49

The document ID (NURI) of the subscribed document.

Accessors

inTransaction
Get Signature

get inTransaction(): boolean

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:211

True, if a transaction is running.

Returns

boolean

readyPromise
Get Signature

get readyPromise(): Promise<void>

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:215

Await to ensure that the subscription is established and the data arrived.

Returns

Promise<void>

signalObject
Get Signature

get signalObject(): DeepSignal<DiscreteArray | DiscreteObject> | undefined

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:110

The signalObject containing all data of the document (once subscription is established). The object behaves like a regular object or array with a couple of additions:

  • Modifications are immediately propagated back to the database.
  • Database changes are immediately reflected in the object.
  • Watch for object changes using watchDeepSignal.
  • Objects in arrays receive a unique @id property.
Returns

DeepSignal<DiscreteArray | DiscreteObject> | undefined

Methods

beginTransaction()

beginTransaction(): void

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:316

Begins a transaction that batches changes to be committed to the database. This is useful for performance reasons.

Note that this does not disable reactivity of the signalObject. Modifications keep being rendered instantly.

Returns

void

close()

close(): void

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:229

Stop the subscription.

If there is more than one subscription with the document ID, the orm subscription won’t close yet.

Additionally, the closing of the subscription is delayed by a couple hundred milliseconds so that when frontend frameworks unmount and soon mount a component again with the same document ID, we reuse the same orm subscription.

Returns

void

commitTransaction()

commitTransaction(): Promise<void>

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:338

Commits a transactions sending all modifications made during the transaction (started with beginTransaction) to the database.

Returns

Promise<void>

Throws

if no transaction is open.

getOrCreate()

static getOrCreate<T>(documentId): DiscreteOrmSubscription

Defined in: sdk/js/orm/src/connector/discrete/discreteOrmSubscriptionHandler.ts:192

Returns an OrmSubscription which subscribes to the given document in a 2-way binding.

You find the document data in the signalObject, once readyPromise resolves. This is a DeepSignal object or array, depending on your CRDT document (e.g. YArray vs YMap). The signalObject behaves like a regular set to the outside but has a couple of additional features:

  • Modifications are propagated back to the document. Note that multiple immediate modifications in the same task, e.g. obj[0] = "foo"; obj[1] = "bar" are batched together and sent in a subsequent microtask.
  • External document changes are immediately reflected in the object.
  • Watch for object changes using watchDeepSignal.

You can use transactions, to prevent excessive calls to the engine with beginTransaction and commitTransaction.

In many cases, you are advised to use a hook for your favorite framework under @ng-org/orm/react|vue|svelte instead of calling getOrCreate directly.

Call `close, to close the subscription.

Note: If another call to getOrCreate was previously made and close was not called on it (or only shortly after), it will return the same OrmSubscription (pooling).

Type Parameters
T

T extends BaseType

Parameters
documentId

string

The document ID (NURI) of the CRDT

Returns

DiscreteOrmSubscription

Example
// We assume you have created a CRDT document already, as below.
// const documentId = await ng.doc_create(
//     session_id,
//     crdt, // "Automerge" | "YMap" | "YArray". YArray is for root arrays, the other two have objects at root.
//     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
//     "store",
//     undefined
// );
const subscription = DiscreteOrmSubscription.getOrCreate(documentId);
// Wait for data.
await subscription.readyPromise;

const document = subscription.signalObject;
if (!document.expenses) {
  document.expenses = [];
}
document.expenses.push({
  name: "New Expense name",
  description: "Expense description",
});

// Await promise to run the below code in a new task.
// That will have push the changes to the database.
await Promise.resolve();

// Here, the expense modifications have been committed
// (unless you had previously called subscription.beginTransaction()).
// The data is available in subscriptions running on a different device too.

subscription.close();

// If you create a new subscription with the same document within a couple of 100ms,
// The subscription hasn't been closed and the old one is returned so that the data
// is available instantly. This is especially useful in the context of unmounting and remounting frontend frameworks.
const subscription2 = DiscreteOrmSubscription.getOrCreate(documentId);

subscription2.signalObject.expenses.push({
  name: "Second expense",
  description: "Second description",
});

subscription2.close();

OrmSubscription

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:46

Class for managing RDF-based ORM subscriptions with the engine.

You have two options on how to interact with the ORM:

  • Use a hook for your favorite framework under @ng-org/orm/react|vue|svelte
  • Call OrmSubscription.getOrCreate to create a subscription manually

For more information about RDF-based ORM subscriptions, see the README and follow the tutorial.

Type Parameters

T

T extends BaseType

Properties

scope

readonly scope: Scope

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:53

The Scope of the subscription.

shapeType

readonly shapeType: ShapeType<T>

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:51

The shape type that is subscribed to.

signalObject

readonly signalObject: DeepSignalSet<T>

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:67

The signalObject containing all data matching the shape and scope (once subscription is established). The object is of type DeepSignalSet which to the outside behaves like a regular set but has a couple of additional features:

  • Modifications are immediately propagated back to the database.
  • Database changes are immediately reflected in the object.
  • .getBy(graphIri, subjectIri) utility for quicker access to objects in set.
  • .first() utility to get the first element added to the set.
  • the iterator utilities, e.g. .map(), .filter(), …
  • Watch for object changes using watchDeepSignal.

Accessors

inTransaction
Get Signature

get inTransaction(): boolean

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:253

True, if a transaction is running.

Returns

boolean

readyPromise
Get Signature

get readyPromise(): Promise<void>

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:257

Await to ensure that the subscription is established and the data arrived.

Returns

Promise<void>

Methods

beginTransaction()

beginTransaction(): void

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:413

Begins a transaction that batches changes to be committed to the database. This is useful for performance reasons.

Note that this does not disable reactivity of the signalObject. Modifications keep being rendered.

Returns

void

close()

close(): void

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:271

Stop the subscription.

If there is more than one subscription with the same shape type and scope the orm subscription will persist.

Additionally, the closing of the subscription is delayed by a couple hundred milliseconds so that when frontend frameworks unmount and soon mount a component again with the same shape type and scope, we reuse the same orm subscription.

Returns

void

commitTransaction()

commitTransaction(): Promise<void>

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:432

Commits a transactions sending all modifications made during the transaction (started with beginTransaction) to the database.

Returns

Promise<void>

getOrCreate()

static getOrCreate<T>(shapeType, scope): OrmSubscription<T>

Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:229

Returns an OrmSubscription which subscribes to the given ShapeType and Scope in a 2-way binding.

You find the data and objects matching the shape and scope in the signalObject once readyPromise resolves. This is a DeepSignalSet which to the outside behaves like a regular set but has a couple of additional features:

  • Modifications are propagated back to the database. Note that multiple immediate modifications in the same task, e.g. obj[0] = "foo"; obj[1] = "bar" are batched together and sent in a subsequent microtask.
  • Database changes are immediately reflected in the object.
  • .getBy(graphIri, subjectIri) utility for quicker access to objects in set.
  • .first() utility to get the first element added to the set.
  • the iterator utilities, e.g. .map(), .filter(), …
  • Watch for object changes using watchDeepSignal.

You can use transactions, to prevent excessive calls to the database with beginTransaction and commitTransaction.

In many cases, you are advised to use a hook for your favorite framework under @ng-org/orm/react|vue|svelte instead of calling getOrCreate directly.

Call close, to close the subscription.

Note: If another call to getOrCreate was previously made and close was not called on it (or only shortly after), it will return the same OrmSubscription.

Type Parameters
T

T extends BaseType

Parameters
shapeType

ShapeType<T>

The ShapeType

scope

Scope

The Scope. If no scope is given, the whole store is considered.

Returns

OrmSubscription<T>

Example
// We assume you have created a graph document already, as below.
// const documentId = await ng.doc_create(
//     session_id,
//     "Graph",
//     "data:graph",
//     "store",
//     undefined
// );
const subscription = OrmSubscription.getOrCreate(ExpenseShapeType, {graphs: [graphIri]});
// Wait for data.
await subscription.readyPromise;

const expense = subscription.signalObject.first()
expense.name = "updated name";
expense.description = "updated description";

// Await promise to run the below code in a new task.
// That will push the changes to the database.
await Promise.resolve();

// Here, the expense modifications have been have been committed
// (unless you had previously called subscription.beginTransaction()).
// The data is available in subscriptions running on a different device too.

subscription.close();
// If you create a new subscription with the same document within a couple of 100ms,
// The subscription hasn't been closed and the old one is returned so that the data
// is available instantly. This is especially useful in the context of frontend frameworks.
const subscription2 = OrmSubscription.getOrCreate(ExpenseShapeType, {graphs: [graphIri]});

subscription2.signalObject.add({
   "@graph": graphIri,
   "@id": "", // Leave empty to auto-assign one.
   name": "A new expense",
   description: "A new description"
});

subscription2.close()

Interfaces

DiscreteObject

Defined in: sdk/js/orm/src/types.ts:59

An allowed object in the CRDT.

Indexable

[key: string]: DiscreteType


DiscreteRootObject

Defined in: sdk/js/orm/src/types.ts:84

The root object for reading and modifying the CRDT as a plain object.

Indexable

[key: string]: string | number | boolean | DiscreteObject | DiscreteRootArray

Type Aliases

DeepSignal

DeepSignal<T> = T extends Function ? T : T extends string | number | boolean ? T : T extends DeepSignalObjectProps<any> | DeepSignalObjectProps<any>[] ? T : T extends infer I[] ? DeepSignal<I>[] : T extends Set<infer S> ? DeepSignalSet<S> : T extends object ? DeepSignalObject<T> : T

Defined in: sdk/js/alien-deepsignals/src/types.ts:228

The object returned by the deepSignal function. It is decorated with utility functions for sets, see DeepSignalSetProps and a __raw__ prop to get the underlying non-reactive object.

Type Parameters

T

T


DeepSignalObject

DeepSignalObject<T> = { [K in keyof T]: DeepSignal<T[K]> }

Defined in: sdk/js/alien-deepsignals/src/types.ts:242

Type Parameters

T

T extends object


DeepSignalSet

DeepSignalSet<T> = DeepSignalSet_<T> & DeepSignalSetProps<T> & DeepSignalObjectProps<T>

Defined in: sdk/js/alien-deepsignals/src/types.ts:193

Type alias for DeepSignal<Set<T>> and reactive Set wrapper that accepts raw or proxied entries. Additionally it is decorated with DeepSignalSetProps and iterator utilities like .map(), .filter(), .some(), …

Note that you can assign plain Sets to properties with type DeepSignalSet, however Typescript will give you a warning. That is a limitation of TypeScript’s capability. Internally, the object will be converted to a DeepSignalSet. You can instruct TypeScript to ignore this with parent.children = new Set() as DeepSignal<Set<any>>.

Type Parameters

T

T


DiscreteCrdt

DiscreteCrdt = "YMap" | "YArray" | "Automerge"

Defined in: sdk/js/orm/src/types.ts:101

The supported discrete (JSON) CRDTs. Automerge and YMap require objects as roots. YArray requires an array as root.


DiscreteRoot

DiscreteRoot = DiscreteRootArray | DiscreteRootObject

Defined in: sdk/js/orm/src/types.ts:94

A discrete document’s root object, either an array or an object.


DiscreteRootArray

DiscreteRootArray = (DiscreteArray | string | number | boolean | DiscreteObject & object)[]

Defined in: sdk/js/orm/src/types.ts:73

The root array for reading and modifying the CRDT as a plain object.


DiscreteType

DiscreteType = DiscreteArray | DiscreteObject | string | number | boolean

Defined in: sdk/js/orm/src/types.ts:63

An allowed type in the CRDT.


Scope

Scope = object

Defined in: sdk/js/orm/src/types.ts:25

When dealing with shapes (RDF-based graph database ORMs): The scope of a shape request. In most cases, it is recommended to use a narrow scope for performance. You can filter results by subjects and graphs. Only objects in that scope will be returned.

Example

// Contains all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
const expenses: DeepSignal<Set<Expense>> = useShape(ExpenseShape, {
  graphs: ["<graph1 NURI>", "<graph2 NURI>"],
  subjects: ["<subject1 IRI>", "<subject2 IRI>"],
});

Properties

graphs?

optional graphs: string[]

Defined in: sdk/js/orm/src/types.ts:32

The graphs to filter for. If more than one NURI is provided, the union of all graphs is considered.

  • Set value to ["did:ng:i"] or [""] for whole dataset.
  • Setting value to [] or leaving it undefined, no objects are returned.
subjects?

optional subjects: string[]

Defined in: sdk/js/orm/src/types.ts:37

Subjects to filter for. Set to [] or leave it undefined for no filtering.

Variables

effect()

const effect: (fn) => () => void = alienEffect

Defined in: sdk/js/alien-deepsignals/src/core.ts:107

Re-export of alien-signals effect function.

Callback reruns on every signal modification that is used within its callback.

Parameters

fn

() => void

Returns

(): void

Returns

void


ngSession

const ngSession: Promise<{ ng: __module; session: Session; }>

Defined in: sdk/js/orm/src/connector/initNg.ts:16

Resolves to the NG session and the ng implementation.

Functions

getObjects()

getObjects<T>(shapeType, scope): Promise<DeepSignalSet<T>>

Defined in: sdk/js/orm/src/connector/getObjects.ts:23

Utility for retrieving objects once without establishing a two-way subscription.

Type Parameters

T

T extends BaseType

Parameters

shapeType

ShapeType<T>

The shape type of the objects to be retrieved.

scope

The scope of the objects to be retrieved as Scope object or as graph NURI string.

string | Scope

Returns

Promise<DeepSignalSet<T>>

A set of all objects matching the shape and scope


getRaw()

getRaw<T>(value): any

Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1474

Get the original, raw value of a deep signal.

Type Parameters

T

T extends object

Parameters

value

T | DeepSignal<T>

Returns

any


initNg()

initNg(ngImpl, session): void

Defined in: sdk/js/orm/src/connector/initNg.ts:51

Initialize the ORM by passing the ng implementation and session.

This is the first thing you need to do before using the ORM.

Parameters

ngImpl

__module

The NextGraph API, e.g. exported from @ng-org/web.

session

Session

The established NextGraph session.

Returns

void

Example

import { ng, init } from "@ng-org/web";
import { initNg as initNgSignals, Session } from "@ng-org/orm";
let session: Session;

// Call as early as possible as it will redirect to the auth page.
await init(
  async (event: any) => {
    session = event.session;
    session!.ng ??= ng;

    // Call initNgSignals
    initNgSignals(ng, session);
  },
  true,
  [],
);

insertObject()

insertObject<T>(shapeType, object): Promise<void>

Defined in: sdk/js/orm/src/connector/insertObject.ts:21

Utility for adding ORM-typed objects to the database without the need for subscribing to documents using an OrmSubscription.

Type Parameters

T

T extends BaseType

Parameters

shapeType

ShapeType<T>

The shape type of the objects to be inserted.

object

T

The object to be inserted.

Returns

Promise<void>


reactUseDiscrete()

reactUseDiscrete<T>(documentId): object

Defined in: sdk/js/orm/src/frontendAdapters/react/useDiscrete.ts:118

Hook to subscribe to an existing discrete (JSON) CRDT document. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT document.

Establishes a 2-way binding: Modifications to the object are immediately committed; changes coming from the engine (or other components) cause an immediate rerender.

In comparison to reactUseShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T = DiscreteRoot

Parameters

documentId

The NURI of the CRDT document.

string | undefined

Returns

object

An object that contains as doc the reactive DeepSignal object or undefined if documentId is undefined.

doc

doc: DeepSignal<T> | undefined = docOrUndefined

Example

// We assume you have created a CRDT document already, as below.
// const documentId = await ng.doc_create(
//     session_id,
//     crdt, // "Automerge" | "YMap" | "YArray". YArray is for root arrays, the other two have objects at root.
//     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
//     "store",
//     undefined
// );

function Expenses({ documentId }: { documentId: string }) {
  const { doc } = useDiscrete(documentId);

  // If the CRDT document is still empty, we need to initialize it.
  if (doc && !doc.expenses) {
    doc.expenses = [];
  }
  const expenses = doc?.expenses;

  const createExpense = useCallback(() => {
    // Note that we use *expense["@id"]* as a key in the expense list.
    // Every object added to a CRDT array gets a stable `@id` property assigned
    // which you can use for referencing objects in arrays even as
    // objects are removed or added from the array.
    // The `@id` is a NURI with the schema `<documentId>:d:<object-specific id>`.
    // Since the `@id` is generated in the engine, the object is
    // *preliminarily given a mock id* which will be replaced immediately.
    expenses.push({
      title: "New expense",
      date: new Date().toISOString(),
    });
  }, [expenses]);

  // Still loading?
  if (!doc) return <div>Loading...</div>;

  return (
    <div>
      <button onClick={() => createExpense()}>+ Add expense</button>
      <div>
        {expenses.length === 0 ? (
          <p>No expenses yet.</p>
        ) : (
          expenses.map((expense) => (
            <ExpenseCard key={expense["@id"]} expense={expense} />
          ))
        )}
      </div>
    </div>
  );
}

In the ExpenseCard component:

function ExpenseCard({expense}: {expense: Expense}) {
   return (
       <input
           value={expense.title}
           onChange={(e) => {
               expense.title = e.target.value; // Changes trigger rerender.
           }}
       />
       <div>
           <p>Date</p>
           <p>{expense.doc}
       </div
   );
}

reactUseShape()

reactUseShape<T>(shape, scope): DeepSignalSet<T>

Defined in: sdk/js/orm/src/frontendAdapters/react/useShape.ts:86

Hook to subscribe to RDF data in the graph database using a shape, see ShapeType.

Returns a DeepSignalSet of objects matching the shape and that are within the scope. Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the engine (or other components) cause an immediate rerender.

Type Parameters

T

T extends BaseType

Parameters

shape

ShapeType<T>

The ShapeType the objects should have (generated by the shex-orm tool).

scope

The Scope as graph string or scope object with graphs and subjects.

string | Scope | undefined

Returns

DeepSignalSet<T>

A DeepSignalSet with the orm objects or an empty set, if still loading.
If the scope is explicitly set to undefined, an empty set is returned which errors if you try to make modifications on it.

Example

function Expenses() {
  const expenses: DeepSignal<Set<Expense>> = useShape(ExpenseShapeType, {
    graphs: ["<graph NURI>"],
  });

  const createExpense = useCallback(() => {
    expenses.add({
      "@graph": `<graph NURI>`,
      "@type": "did:ng:z:Expense",
      "@id": "", // Assigns ID automatically, if set to "".
      title: "New expense",
      dateOfPurchase: obj.dateOfPurchase ?? new Date().toISOString(),
    });
  }, [expenses]);

  const expensesSorted = [...expenses].sort((a, b) =>
    a.dateOfPurchase.localeCompare(b.dateOfPurchase),
  );

  // Note that if you use `@id` (the subject IRI) as key, you need to ensure that it is unique within your scope.
  // If it is not (i.e. there are two graphs with the same subject), use the combination of `@graph` and `@id`.

  return (
    <div>
      <button onClick={() => createExpense({})}>+ Add expense</button>
      <div>
        {expensesSorted.length === 0 ? (
          <p>No expenses yet.</p>
        ) : (
          expensesSorted.map((expense) => (
            // You can modify the expense's properties in the ExpenseCard component
            // which will instantly trigger a rerender.
            <ExpenseCard key={expense["@id"]} expense={expense} />
          ))
        )}
      </div>
    </div>
  );
}

svelte4UseDiscrete()

svelte4UseDiscrete<T>(documentIdOrPromise): object

Defined in: sdk/js/orm/src/frontendAdapters/svelte4/useDiscrete.svelte.ts:100

Svelte 4 hook to subscribe to discrete (JSON) CRDT documents. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT.

Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the backend (or other components) cause an immediate rerender.

In comparison to svelte4UseShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T = DiscreteRoot

Parameters

documentIdOrPromise

The NURI of the CRDT document or a promise to that.

string | Promise<string> | undefined

Returns

object

The store of the reactive JSON object of the CRDT document or undefined.

doc

doc: UseDeepSignalResult<T | undefined>

Example

<script lang="ts">
    // We assume you have created a CRDT document already, as below.
    // const documentId = await ng.doc_create(
    //     session_id,
    //     crdt, // "Automerge" | "YMap" | "YArray"
    //     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
    //     "store",
    //     undefined

    const doc = useDiscrete(documentIdPromise);

    // If the CRDT document is still empty, we need to initialize it.
    $: if (doc && !doc.expenses) {
        doc.expenses = [];
    }

    // Call doc.expenses.push({title: "Example title"}), to add new elements.

    // Note that we use expense["@id"] NURI as a key in the expense list.
    // Every object added to a CRDT array gets a stable `@id` property assigned
    // which you can use for referencing objects in arrays even as
    // objects are removed from the array.
    // Since the `@id` is generated in the backend, the object is preliminarily
    // given a mock ID which will be replaced immediately
</script>

<section>
    <div>
        {#if !doc}
            Loading...
        {:else if doc.expenses.length === 0}
            <p>
                Nothing tracked yet - log your first purchase to kick things
                off.
            </p>
        {:else}
            {#each doc.expenses as expense, index (expense["@id"])}
                <ExpenseCard {expense} />
            {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:

    let {
        expense = $bindable(),
    }: { expense: Expense; } = $props();
</script>

<div>
    <input
        value={expense.title ?? ""}
        oninput={(event) => {expense.title = event.currentTarget?.value ?? ""}}
        placeholder="Expense title"
    />
</div>

svelte4UseShape()

svelte4UseShape<T>(shape, scope): UseShapeStoreResult<Set<T>>

Defined in: sdk/js/orm/src/frontendAdapters/svelte4/useShape.svelte.ts:94

Svelte 4 hook to subscribe to RDF data in the graph database using a shape, see ShapeType.

Returns a DeepSignalSet store containing the objects matching the shape and that are within the scope. Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the backend (or other components) cause an immediate rerender.

Type Parameters

T

T extends BaseType

Parameters

shape

ShapeType<T>

The ShapeType the objects should have (generated by the shex-orm tool).

scope

The Scope as graph string or scope object with graphs and subjects.

string | Scope | undefined

Returns

UseShapeStoreResult<Set<T>>

A DeepSignalSet with the orm objects or an empty set, if still loading.
If the scope is explicitly set to undefined, an empty set is returned which errors if you try to make modifications on it.

Example

<script lang="ts">
    // Gets all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
    const expenses: DeepSignal<Set<Expense>> = useShape(ExpenseShape,
        {graphs: ["<g1 NURI>", "<g2 NURI>"],
        subjects: ["<s1 NURI>", "<s2 NURI>"]});

    const expensesSorted = computed(() => expenses.sort((a, b) =>
        a.dateOfPurchase.localeCompare(b.dateOfPurchase)
    ));

    // Call expenses.add({"@graph": "<g1 or g2 NURI>", "@id": "", title: "Example title"}), to add new elements.
    // Leave `@id` an empty string to auto-generate a subject IRI (adjust your scope accordingly).

    // Note that if you use `@id` (the subject IRI) as key, you need to ensure that it is unique within your scope.
    // If it is not (i.e. there are two graphs with the same subject), use the combination of `@graph` and `@id`.
</script>

<section>
    <div>
        {# if expensesSorted.length === 0}
        <p>
            No expense yet.
        </p>
        {:else}
        {#each expensesSorted as expense, index (expense['@id']) }
            <ExpenseCard
                expense={expense}
            />
        {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:


  let {
    expense = $bindable(),
  }: { expense: Expense; } = $props();
</script>

<div>
  <input
    bind:value={expense.title}
    placeholder="Expense title"
  />
</div>

svelteUseDiscrete()

svelteUseDiscrete<T>(documentIdOrPromise): object

Defined in: sdk/js/orm/src/frontendAdapters/svelte/useDiscrete.svelte.ts:105

Svelte 5 hook to subscribe to existing discrete (JSON) CRDT documents. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT.

Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the engine (or other components) cause an immediate rerender.

In comparison to svelteUseShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T = DiscreteRoot

Parameters

documentIdOrPromise

The NURI of the CRDT document or a promise to that.

string | Promise<string> | undefined

Returns

object

The reactive JSON object of the CRDT document.

doc

doc: DeepSignal<T | undefined>

Example

<script lang="ts">
    // We assume you have created a CRDT document already, as below.
    // const documentId = await ng.doc_create(
    //     session_id,
    //     crdt, // "Automerge" | "YMap" | "YArray"
    //     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
    //     "store",
    //     undefined,
    // );

    const { doc } = useDiscrete(documentIdPromise);

    $effect(() => {
        // If the CRDT document is still empty, we need to initialize it.
        if (doc && !doc.expenses) {
            doc.expenses = [];
        }
    });

    const createExpense = () => {
        // Note that we use *expense["@id"]* as a key in the expense list.
        // Every object added to a CRDT array gets a stable `@id` property assigned
        // which you can use for referencing objects in arrays even as
        // preceding objects are removed or added from the array.
        // The `@id` is an NURI with the schema `<documentId>:d:<object-specific id>`.
        // Since the `@id` is generated in the engine, the object is
        // *preliminarily given a mock id* which will be replaced immediately.
        expenses.push({
            title: "New expense",
            date: new Date().toISOString(),
        });
    };
</script>

<section>
    <div>
        <button on:click={() => createExpense({})} />

        {#if !doc}
            Loading...
        {:else if doc.expenses.length === 0}
            <p>
                Nothing tracked yet - log your first purchase to kick things
                off.
            </p>
        {:else}
            {#each doc.expenses as expense, index (expense["@id"])}
                <ExpenseCard {expense} />
            {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:

    let {
        expense = $bindable(),
    }: { expense: Expense; } = $props();
</script>

<div>
    <input
        bind:value={expense.title}
    />
</div>

svelteUseShape()

svelteUseShape<T>(shape, scope): DeepSignalSet<T>

Defined in: sdk/js/orm/src/frontendAdapters/svelte/useShape.svelte.ts:96

Svelte 5 hook to subscribe to RDF data in the graph database using a shape, see ShapeType.

Returns a DeepSignalSet that contain the objects matching the shape and that are within the scope. Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the engine (or other components) cause an immediate rerender.

Type Parameters

T

T extends BaseType

Parameters

shape

ShapeType<T>

The ShapeType the objects should have (generated by the @ng-org/shex-orm tool).

scope

The Scope as graph string or scope object with graphs and subjects.

string | Scope | undefined

Returns

DeepSignalSet<T>

A DeepSignalSet with the orm objects or an empty set, if still loading.
If the scope is explicitly set to undefined, an empty set is returned which errors if you try to make modifications on it.

Example

<script lang="ts">
    // Gets all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
    const expenses: DeepSignal<Set<Expense>> = useShape(ExpenseShapeType,
        {graphs: ["<g1 NURI>", "<g2 NURI>"],
        subjects: ["<s1 NURI>", "<s2 NURI>"]});

    const expensesSorted = computed(() => expenses.sort((a, b) =>
        a.dateOfPurchase.localeCompare(b.dateOfPurchase)
    ));

    const createExpense = () => {
        expenses.add({
            "@graph": `<graph NURI>`,
            "@type": "did:ng:z:Expense",
            "@id": "", // Assigns ID automatically, if set to "".
            title: "New expense",
            dateOfPurchase: obj.dateOfPurchase ?? new Date().toISOString(),
        });
    };

    // Note that if you use `@id` (the subject IRI) as key, you need to ensure that it is unique within your scope.
    // If it is not (i.e. there are two graphs with the same subject), use the combination of `@graph` and `@id`.
</script>

<section>
    <div>
        <button on:click={() => createExpense()}>
            + Add expense
        </button>

        {# if expensesSorted.length === 0}
            <p>
                No expense yet.
            </p>
        {:else}
            {#each expensesSorted as expense, index (expense['@id']) }
                <ExpenseCard
                    expense={expense}
                />
            {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:

<script lang="ts">
    let { expense }: { expense: DeepSignal<Expense> } = $props();
</script>

<div>
    <input bind:value={expense.title} />
</div>

vueUseDiscrete()

vueUseDiscrete<T>(documentId): ToRefs<{ doc: T; }>

Defined in: sdk/js/orm/src/frontendAdapters/vue/useDiscrete.ts:118

Hook to subscribe to an existing discrete (JSON) CRDT document. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT document.

Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the engine (or other components) cause an immediate rerender.

In comparison to useShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T = DiscreteRoot

Parameters

documentId

MaybeRefOrGetter<string | undefined>

The NURI of the CRDT document or undefined as MaybeRefOrGetter.

Returns

ToRefs<{ doc: T; }>

An object that contains as data the reactive DeepSignal object or undefined if not loaded yet or documentId is undefined.

Example

<script lang="ts">
  // We assume you have created a CRDT document already, as below.
  // const documentId = await ng.doc_create(
  //     session_id,
  //     crdt, // "Automerge" | "YMap" | "YArray"
  //     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
  //     "store",
  //     undefined
  // );
  const { doc } = useDiscrete(documentId);

  // If document is new, we need to set up the basic structure.
  effect(() => {
    if (doc.value && !doc.value.expenses) {
      doc.value.expenses = [];
    }
  });

  const createExpense = () => {
    // Note that we use *expense["@id"]* as a key in the expense list.
    // Every object added to a CRDT array gets a stable `@id` property assigned
    // which you can use for referencing objects in arrays even as
    // objects are removed or added from the array.
    // The `@id` is an NURI with the schema `<documentId>:d:<object-specific id>`.
    // Since the `@id` is generated in the engine, the object is
    // *preliminarily given a mock id* which will be replaced immediately.
    doc.value.expenses.push({
      title: "New expense",
      date: new Date().toISOString(),
    });
  };
</script>

<template>
  <div v-if="!doc">Loading...</div>
  <div v-else>
    <p v-if="expenses.length === 0">No expenses yet.</p>
    <template v-else>
      <button @click="{()" ="">createExpense()} > + Add expense</button>
      <ExpenseCard
        v-for="expense in expenses"
        :key="expense['@id']"
        :expense="expense"
      />
    </template>
  </div>
</template>

In the ExpenseCard component:

<script lang="ts">
  const { expense } = defineProps<{
    expense: DeepSignal<Expense>;
  }>();

  // If you modify expense in the component,
  // the changes are immediately propagated to other consuming components
  // And persisted in the database.
</script>

<template>
  <input v-model="expense.title" placeholder="Expense title" />
</template>

vueUseShape()

vueUseShape<T>(shape, scope): DeepSignalSet<T>

Defined in: sdk/js/orm/src/frontendAdapters/vue/useShape.ts:89

Hook to subscribe to RDF data in the graph database using a shape, see ShapeType. The returned objects are as easy to use as other TypeScript objects.

Returns a DeepSignalSet of objects matching the shape and that are within the scope. Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the backend (or other components) cause an immediate rerender.

Type Parameters

T

T extends BaseType

Parameters

shape

ShapeType<T>

The ShapeType the objects should have (generated by the shex-orm tool).

scope

The Scope as graph string or scope object with graphs and subjects.

string | Scope | undefined

Returns

DeepSignalSet<T>

A DeepSignalSet with the orm objects or an empty set, if still loading.
If the scope is explicitly set to undefined, an empty set is returned which errors if you try to make modifications on it.

Example

<script lang="ts">
  // Contains all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
  const expenses: DeepSignal<Set<Expense>> = useShape(ExpenseShapeType, {
    graphs: ["<g1 NURI>", "<g2 NURI>"],
    subjects: ["<s1 IRI>", "<s2 IRI>"],
  });

  const expensesSorted = computed(() =>
    [...expenses].sort((a, b) =>
      a.dateOfPurchase.localeCompare(b.dateOfPurchase),
    ),
  );

  // Simply call expenses.add({"@graph": "<g1 or g2 NURI>", "@id": "", title: "Example title"}), to add new elements.
  // Leave `@id` an empty string to auto-generate a subject NURI (adjust your scope accordingly).

  // Note that if you use `@id` (the subject IRI) as key, you need to ensure that it is unique within your scope.
  // If it is not (i.e. there are two graphs with the same subject), use the combination of `@graph` and `@id`.
</script>

<template>
  <div>
    <p v-if="expensesSorted.length === 0">No expenses yet.</p>
    <template v-else>
      <ExpenseCard
        v-for="expense in expensesSorted"
        :key="expense['@id'])"
        :expense="expense"
      />
    </template>
  </div>
</template>

In the ExpenseCard component:

<script lang="ts">
  const { expense } = defineProps<{
    expense: DeepSignal<Expense>;
  }>();

  // If you modify expense in the component,
  // the changes are immediately propagated to other consuming components.
  // And persisted in the database.
</script>

<template>
  <input v-model="expense.title" placeholder="Expense title" />
</template>

watch()

watch<T>(source, callback, options?): object

Defined in: sdk/js/alien-deepsignals/src/watch.ts:89

Watch for changes to a deepSignal.

Whenever a change is made, callback is called with the patches describing the change and the new value. If you set triggerInstantly, the callback is called on every property change. If not, all changes are aggregated and callback is called in a microtask when the current task finishes, e.g. await is called (meaning it supports batching).

When objects are added to Sets, their synthetic ID (usually @id) becomes part of the patch path. This allows patches to uniquely identify which Set entry is being mutated.

const state = deepSignal(
    { s: new Set() },
    { ...}
);

watch(state, ({ patches }) => {
    console.log(JSON.stringify(patches));
});

state.s.add({ data: "test" });
// Will log:
// [
//   {"path":["s","did:ng:o:123"],"op":"add","type":"object"},
//   {"path":["s","did:ng:o:123","@id"],"op":"add","value":"did:ng:o:123"},
//   {"path":["s","did:ng:o:123","data"],"op":"add","value":"test"}
// ]

state.s.getById("did:ng:o:123")!.data = "new value"
// Will log:
// [
//   {"path":["s","did:ng:o:123","data"],"op":"add","value":"new value"}
// ]

Type Parameters

T

T extends object

Parameters

source

DeepSignalSet<T> | DeepSignalObject<T> | DeepSignal<T>

callback

WatchPatchCallback<T>

options?

WatchOptions = {}

Returns

object

registerCleanup

registerCleanup: RegisterCleanup

stopListening()

stopListening: () => void

Returns

void