Core Linked package for the query DSL, SHACL shape decorators/metadata, and package registration.
Linked core gives you a type-safe, schema-parameterized query language and SHACL-driven Shape classes for linked data. It compiles queries into a plain JS query object that can be executed by a store.
- Schema-Parameterized Query DSL: TypeScript-embedded queries driven by your Shape definitions.
- Shape Classes (SHACL): TypeScript classes that generate SHACL shape metadata.
- Object-Oriented Data Operations: Query, create, update, and delete data using the same Shape-based API.
- Storage Routing:
LinkedStorageroutes query objects to your configured store(s) that implementIQuadStore. - Automatic Data Validation: SHACL shapes can be synced to your store for schema-level validation, and enforced at runtime by stores that support it.
npm install @_linked/coreimport {Shape, LinkedStorage} from '@_linked/core';
import {linkedPackage} from '@_linked/core/utils/Package';@_linked/rdf-mem-store: in-memory RDF store that implementsIQuadStore.@_linked/react: React bindings for Linked queries and shapes.
Linked packages expose shapes, utilities, and ontologies through a small package.ts file. This makes module exports discoverable across Linked modules and enables linked decorators.
Minimal package.ts
import {linkedPackage} from '@_linked/core/utils/Package';
export const {
linkedShape,
linkedUtil,
linkedOntology,
registerPackageExport,
registerPackageModule,
packageExports,
getPackageShape,
} = linkedPackage('my-package-name');Decorators and helpers
@linkedShape: registers a Shape class and generates SHACL shape metadata@linkedUtil: exposes utilities to other Linked moduleslinkedOntology(...): registers an ontology and (optionally) its data loaderregisterPackageExport(...): manually export something into the Linked package treeregisterPackageModule(...): lower-level module registrationgetPackageShape(...): resolve a Shape class by name to avoid circular imports
Linked uses Shape classes to generate SHACL metadata. Paths, target classes, and node kinds are expressed as NodeReferenceValue objects: {id: string}.
import {Shape} from '@_linked/core';
import {ShapeSet} from '@_linked/core/collections/ShapeSet';
import {literalProperty, objectProperty} from '@_linked/core/shapes/SHACL';
import {createNameSpace} from '@_linked/core/utils/NameSpace';
import {linkedShape} from './package';
const schema = createNameSpace('https://schema.org/');
const PersonClass = schema('Person');
const name = schema('name');
const knows = schema('knows');
@linkedShape
export class Person extends Shape {
static targetClass = PersonClass;
@literalProperty({path: name, required: true, maxCount: 1})
declare name: string;
@objectProperty({path: knows, shape: Person})
declare knows: ShapeSet<Person>;
}Queries are expressed with the same Shape classes and compile to a query object that a store executes.
/* Result: Array<{id: string; name: string}> */
const names = await Person.select((p) => p.name);
const myNode = {id: 'https://my.app/node1'};
/* Result: {id: string; name: string} | null */
const person = await Person.select(myNode, (p) => p.name);
const missing = await Person.select({id: 'https://my.app/missing'}, (p) => p.name); // null
/* Result: {id: string} & UpdatePartial<Person> */
const created = await Person.create({
name: 'Alice',
knows: [{id: 'https://my.app/node2'}],
});
const updated = await Person.update(myNode, {
name: 'Alicia',
});
// Overwrite a multi-value property
const overwriteFriends = await Person.update(myNode, {
knows: [{id: 'https://my.app/node2'}],
});
// Add/remove items in a multi-value property
const addRemoveFriends = await Person.update(myNode, {
knows: {
add: [{id: 'https://my.app/node3'}],
remove: [{id: 'https://my.app/node2'}],
},
});
/* Result: {deleted: Array<{id: string}>, count: number} */
await Person.delete(myNode);LinkedStorage is the routing helper (not an interface). It forwards query objects to a store that implements IQuadStore.
import {LinkedStorage} from '@_linked/core';
import {InMemoryStore} from '@_linked/rdf-mem-store';
LinkedStorage.setDefaultStore(new InMemoryStore());You can also route specific shapes to specific stores:
LinkedStorage.setStoreForShapes(new InMemoryStore(), Person);SHACL shapes are ideal for data validation. Linked generates SHACL shapes from your TypeScript Shape classes, which you can sync to your store for schema-level validation. When your store enforces those shapes at runtime, you get both schema validation and runtime enforcement for extra safety.
The query DSL is schema-parameterized: you define your own SHACL shapes, and Linked exposes a type-safe, object-oriented query API for those shapes.
- Basic selection (literals, objects, dates, booleans)
- Target a specific subject by
{id}or instance - Multiple paths and mixed results
- Nested paths (deep selection)
- Sub-queries on object/set properties
- Filtering with
where(...)andequals(...) and(...)/or(...)combinations- Set filtering with
some(...)/every(...)(and implicitsome) - Outer
where(...)chaining - Counting with
.size() - Custom result formats (object mapping)
- Type casting with
.as(Shape) - Sorting, limiting, and
.one() - Query context variables
- Preloading (
preloadFor) for component-like queries - Create / Update / Delete mutations
Result types are inferred from your Shape definitions and the selected paths. Examples below show abbreviated result shapes.
/* Result: Array<{id: string; name: string}> */
const names = await Person.select((p) => p.name);
/* Result: Array<{id: string; knows: Array<{id: string}>}> */
const friends = await Person.select((p) => p.knows);
const dates = await Person.select((p) => [p.birthDate, p.name]);
const flags = await Person.select((p) => p.isRealPerson);const myNode = {id: 'https://my.app/node1'};
/* Result: {id: string; name: string} | null */
const one = await Person.select(myNode, (p) => p.name);
const missing = await Person.select({id: 'https://my.app/missing'}, (p) => p.name); // null/* Result: Array<{id: string; name: string; knows: Array<{id: string}>; bestFriend: {id: string; name: string}}> */
const mixed = await Person.select((p) => [p.name, p.knows, p.bestFriend.name]);
const deep = await Person.select((p) => p.knows.bestFriend.name);const detailed = await Person.select((p) =>
p.knows.select((f) => f.name),
);const filtered = await Person.select().where((p) => p.name.equals('Semmy'));
const byRef = await Person.select().where((p) =>
p.bestFriend.equals({id: 'https://my.app/node3'}),
);const andQuery = await Person.select((p) =>
p.knows.where((f) =>
f.name.equals('Moa').and(f.hobby.equals('Jogging')),
),
);
const orQuery = await Person.select((p) =>
p.knows.where((f) =>
f.name.equals('Jinx').or(f.hobby.equals('Jogging')),
),
);const implicitSome = await Person.select().where((p) =>
p.knows.name.equals('Moa'),
);
const explicitSome = await Person.select().where((p) =>
p.knows.some((f) => f.name.equals('Moa')),
);
const every = await Person.select().where((p) =>
p.knows.every((f) => f.name.equals('Moa').or(f.name.equals('Jinx'))),
);const outer = await Person.select((p) => p.knows).where((p) =>
p.name.equals('Semmy'),
);/* Result: Array<{id: string; knows: number}> */
const count = await Person.select((p) => p.knows.size());/* Result: Array<{id: string; nameIsMoa: boolean; numFriends: number}> */
const custom = await Person.select((p) => ({
nameIsMoa: p.name.equals('Moa'),
numFriends: p.knows.size(),
}));const guards = await Person.select((p) => p.pets.as(Dog).guardDogLevel);const sorted = await Person.select((p) => p.name).sortBy((p) => p.name, 'ASC');
const limited = await Person.select((p) => p.name).limit(1);
const single = await Person.select((p) => p.name).one();Query context lets you inject request-scoped values (like the current user) into filters without threading them through every call.
setQueryContext('user', {id: 'https://my.app/user1'}, Person);
const ctx = await Person.select((p) => p.name).where((p) =>
p.bestFriend.equals(getQueryContext('user')),
);Preloading appends another query to the current query so the combined data is loaded in one round-trip. This is helpful when rendering a nested tree of components and loading all data at once.
const preloaded = await Person.select((p) => [
p.hobby,
p.bestFriend.preloadFor(ChildComponent),
]);/* Result: {id: string} & UpdatePartial<Person> */
const created = await Person.create({name: 'Alice'});
const updated = await Person.update({id: 'https://my.app/node1'}, {name: 'Alicia'});
// Overwrite a multi-value property
const overwriteFriends = await Person.update({id: 'https://my.app/node1'}, {
knows: [{id: 'https://my.app/node2'}],
});
// Add/remove items in a multi-value property
const addRemoveFriends = await Person.update({id: 'https://my.app/node1'}, {
knows: {
add: [{id: 'https://my.app/node3'}],
remove: [{id: 'https://my.app/node2'}],
},
});
await Person.delete({id: 'https://my.app/node1'});- Allow
preloadForto accept another query (not just a component). - Make and expose functions for auto syncing shapes to the graph.
See CHANGELOG.md.