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
- Start
- RDF (graph) ORM: Defining Schemas
- Frontend Framework Usage
- Working with Data
- Reference
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.
- Discrete CRDTs
- all frameworks running in the same window with Astro
- Svelte 5
- Svelte 4 (no support for Svelte 3)
- Vue
- React
- RDF CRDT
- all frameworks running in the same window with Astro
- Svelte 5
- Svelte 4 (no support for Svelte 3)
- Vue
- React
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:
- discrete CRDTs for
- Svelte 5: useDiscrete
- Svelte 4: useDiscrete
- Vue: useDiscrete
- React: useDiscrete
- graph CRDT for:
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()oruseDiscrete()hook inside of a component. - For graph ORMs (no 2-way binding):
getObjects(shapeType, scope)Gets all object with the given shape type within thescope. The returned objects are notDeepSignalobjects - 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 withuseShape()andOrmSubscription, you can just add objects to the returned set orsubscription.signalObject, respectively. This function spares you of creating anOrmSubscriptionand can be used outside of components, where you can’t calluseShape.
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.
- iterator helper methods (e.g.
- 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
readonlydocumentId: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
@idproperty.
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()
staticgetOrCreate<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
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
readonlyscope:Scope
Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:53
The Scope of the subscription.
shapeType
readonlyshapeType:ShapeType<T>
Defined in: sdk/js/orm/src/connector/ormSubscriptionHandler.ts:51
The shape type that is subscribed to.
signalObject
readonlysignalObject: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()
staticgetOrCreate<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
The Scope. If no scope is given, the whole store is considered.
Returns
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> =TextendsFunction?T:Textendsstring|number|boolean?T:TextendsDeepSignalObjectProps<any> |DeepSignalObjectProps<any>[] ?T:Textends infer I[] ?DeepSignal<I>[] :TextendsSet<infer S> ?DeepSignalSet<S> :Textendsobject?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?
optionalgraphs: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 itundefined, no objects are returned.
subjects?
optionalsubjects: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()
consteffect: (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
constngSession: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
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
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
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
options?
WatchOptions = {}
Returns
object
registerCleanup
registerCleanup:
RegisterCleanup
stopListening()
stopListening: () =>
void
Returns
void