Skip to content

Releases: Semantu/linked

v2.4.1

06 Apr 16:08
9a5410a

Choose a tag to compare

Patch Changes

  • #60 ec239d3 Thanks @flyon! - Fix QueryBuilder.toJSON() to serialize where, orderBy, minus, and preload clauses that were previously silently dropped during JSON round-trips

  • #64 a8d9ad9 Thanks @flyon! - Refine SPARQL select lowering so top-level null-rejecting filters emit required triples instead of redundant OPTIONAL bindings. Queries like Person.select().where((p) => p.name.equals('Semmy')) now lower to a required ?a0 <name> ?a0_name triple, while cases that still need nullable behavior such as p.name.equals('Jinx').or(p.hobby.equals('Jogging')) remain optional.

    This change does not add new DSL APIs, but it does change the generated SPARQL shape for some outer where() clauses to better match hand-written intent. Inline traversal .where(...), EXISTS filters, and aggregate HAVING paths keep their previous behavior.

    See documentation/sparql-algebra.md for the updated lowering rules and examples.

v2.4.0

03 Apr 15:07
97b0ded

Choose a tag to compare

Minor Changes

  • #53 44da872 Thanks @flyon! - ### New: .none() collection quantifier

    Added .none() on QueryShapeSet for filtering where no elements match a condition:

    // "People who have NO friends that play chess"
    Person.select((p) => p.name).where((p) =>
      p.friends.none((f) => f.hobby.equals("Chess"))
    );

    Generates FILTER(NOT EXISTS { ... }) in SPARQL. Equivalent to .some(fn).not().

    Changed: .equals() now returns ExpressionNode (was Evaluation)

    .equals() on query proxies now returns ExpressionNode instead of Evaluation, enabling .not() chaining:

    // Now works — .equals() chains with .not()
    .where(p => p.name.equals('Alice').not())
    .where(p => Expr.not(p.name.equals('Alice')))

    Changed: .some() / .every() now return ExistsCondition (was SetEvaluation)

    .some() and .every() on collections now return ExistsCondition which supports .not():

    .where(p => p.friends.some(f => f.name.equals('Alice')).not()) // same as .none()

    Breaking: Evaluation class removed

    The Evaluation class and related types (SetEvaluation, WhereMethods, WhereEvaluationPath) have been removed. Code that imported or depended on these types must migrate to ExpressionNode / ExistsCondition. The WhereClause type now accepts ExpressionNode | ExistsCondition | callback.

    New exports

    • ExistsCondition — from @_linked/core/expressions/ExpressionNode
    • isExistsCondition() — type guard for ExistsCondition

v2.2.3

28 Mar 23:51
91b16cc

Choose a tag to compare

Patch Changes

  • #42 1b4d114 Thanks @flyon! - Add PendingQueryContext for lazy query context resolution. getQueryContext() now returns a live reference with a lazy .id getter instead of null when the context hasn't been set yet. QueryBuilder.for() accepts PendingQueryContext and null. New hasPendingContext() method. setQueryContext(name, null) now properly clears the entry. Test Fuseki port changed to 3939; globalSetup/globalTeardown added for reliable Fuseki auto-start.

v2.2.1

25 Mar 03:40
eb6c674

Choose a tag to compare

Patch Changes

  • #37 0a3adc1 Thanks @flyon! - Fix SPARQL generation for .where() filters with OR conditions and .every()/.some() quantifiers.
    Tightened assertions across multiple integration tests.

v2.2.0

20 Mar 00:31
6a303ce

Choose a tag to compare

Patch Changes

  • #34 e2ae4a2 Thanks @flyon! - ### SHACL property path support

    Property decorators now accept full SPARQL property path syntax:

    @literalProperty({path: 'foaf:knows/foaf:name'})        // sequence
    @literalProperty({path: '<http://ex.org/a>|<http://ex.org/b>'})  // alternative
    @literalProperty({path: '^foaf:knows'})                  // inverse
    @literalProperty({path: 'foaf:knows*'})                  // zeroOrMore

    New exports from src/paths/:

    • PathExpr, PathRef — AST types for property paths
    • parsePropertyPath(input): PathExpr — parser for SPARQL property path strings
    • normalizePropertyPath(input): PathExpr — normalizes any input form to canonical AST
    • pathExprToSparql(expr): string — renders PathExpr to SPARQL syntax
    • serializePathToSHACL(expr): SHACLPathResult — serializes to SHACL RDF triples

    PropertyShape.path is now typed as PathExpr (was opaque). Complex paths flow through the full IR pipeline and emit correct SPARQL property path syntax in generated queries.

    Strict prefix resolution in query API

    QueryBuilder.for() and .forAll() now throw on unregistered prefixes instead of silently passing through. New export:

    • resolveUriOrThrow(str): string — strict prefix resolution (throws on unknown prefix)

    SHACL constraint field fixes

    • hasValue and in config fields now correctly handle literal values (string, number, boolean) — previously all values were wrapped as IRI nodes
    • lessThan and lessThanOrEquals config fields are now wired into createPropertyShape and exposed via getResult()
    • New PropertyShapeResult interface provides typed access to getResult() output

v2.0.1

11 Mar 08:36
7a70313

Choose a tag to compare

Patch Changes

  • #27 d3c1e91 Thanks @flyon! - Add MINUS support on QueryBuilder with multiple call styles:

    • .minus(Shape) — exclude by shape type
    • .minus(p => p.prop.equals(val)) — exclude by condition
    • .minus(p => p.prop) — exclude by property existence
    • .minus(p => [p.prop1, p.nested.prop2]) — exclude by multi-property existence with nested path support

    Add bulk delete operations:

    • Shape.deleteAll() / DeleteBuilder.from(Shape).all() — delete all instances with schema-aware blank node cleanup
    • Shape.deleteWhere(fn) / DeleteBuilder.from(Shape).where(fn) — conditional delete

    Add conditional update operations:

    • .update(data).where(fn) — update matching instances
    • .update(data).forAll() — update all instances

    API cleanup:

    • Deprecate sortBy() in favor of orderBy()
    • Remove DeleteBuilder.for() — use DeleteBuilder.from(shape, ids) instead
    • Require data parameter in Shape.update(data)

v2.0.0

10 Mar 23:15
92d34a9

Choose a tag to compare

Major Changes

  • #23 d2d1eca Thanks @flyon! - ## Breaking Changes

    Shape.select() and Shape.update() no longer accept an ID as the first argument

    Use .for(id) to target a specific entity instead.

    Select:

    // Before
    const result = await Person.select({ id: "..." }, (p) => p.name);
    
    // After
    const result = await Person.select((p) => p.name).for({ id: "..." });

    .for(id) unwraps the result type from array to single object, matching the old single-subject overload behavior.

    Update:

    // Before
    const result = await Person.update({ id: "..." }, { name: "Alice" });
    
    // After
    const result = await Person.update({ name: "Alice" }).for({ id: "..." });

    Shape.selectAll(id) also no longer accepts an id — use Person.selectAll().for(id).

    ShapeType renamed to ShapeConstructor

    The type alias for concrete Shape subclass constructors has been renamed. Update any imports or references:

    // Before
    import type { ShapeType } from "@_linked/core/shapes/Shape";
    
    // After
    import type { ShapeConstructor } from "@_linked/core/shapes/Shape";

    QueryString, QueryNumber, QueryBoolean, QueryDate classes removed

    These have been consolidated into a single generic QueryPrimitive<T> class. If you were using instanceof checks against these classes, use instanceof QueryPrimitive instead and check the value's type.

    Internal IR types removed

    The following types and functions have been removed from SelectQuery. These were internal pipeline types — if you were using them for custom store integrations, the replacement is FieldSetEntry[] (available from FieldSet):

    • Types: SelectPath, QueryPath, CustomQueryObject, SubQueryPaths, ComponentQueryPath
    • Functions: fieldSetToSelectPath(), entryToQueryPath()
    • Methods: QueryBuilder.getQueryPaths(), BoundComponent.getComponentQueryPaths()
    • RawSelectInput.select field renamed to RawSelectInput.entries (type changed from SelectPath to FieldSetEntry[])

    getPackageShape() return type is now nullable

    Returns ShapeConstructor | undefined instead of typeof Shape. Code that didn't null-check the return value will now get TypeScript errors.

    New Features

    .for(id) and .forAll(ids) chaining

    Consistent API for targeting entities across select and update operations:

    // Single entity (result is unwrapped, not an array)
    await Person.select((p) => p.name).for({ id: "..." });
    await Person.select((p) => p.name).for("https://...");
    
    // Multiple specific entities
    await QueryBuilder.from(Person)
      .select((p) => p.name)
      .forAll([{ id: "..." }, { id: "..." }]);
    
    // All instances (default — no .for() needed)
    await Person.select((p) => p.name);

    Dynamic Query Building with QueryBuilder and FieldSet

    Build queries programmatically at runtime — for CMS dashboards, API endpoints, configurable reports. See the Dynamic Query Building section in the README for full documentation and examples.

    Key capabilities:

    • QueryBuilder.from(Person) or QueryBuilder.from('https://schema.org/Person') — fluent, chainable, immutable query construction
    • FieldSet.for(Person, ['name', 'knows']) — composable field selections with .add(), .remove(), .pick(), FieldSet.merge()
    • FieldSet.all(Person, {depth: 2}) — select all decorated properties with optional depth
    • JSON serialization: query.toJSON() / QueryBuilder.fromJSON(json) and fieldSet.toJSON() / FieldSet.fromJSON(json)
    • All builders are PromiseLikeawait them directly or call .build() to inspect the IR

    Mutation Builders

    CreateBuilder, UpdateBuilder, and DeleteBuilder provide the programmatic equivalent of Person.create(), Person.update(), and Person.delete(), accepting Shape classes or shape IRI strings. See the Mutation Builders section in the README.

    PropertyPath exported

    The PropertyPath value object is now a public export — a type-safe representation of a sequence of property traversals through a shape graph.

    import { PropertyPath, walkPropertyPath } from "@_linked/core";

    ShapeConstructor<S> type

    New concrete constructor type for Shape subclasses. Eliminates ~30 as any casts across the codebase and provides better type safety at runtime boundaries (builder .from() methods, Shape static methods).

v1.3.0

04 Mar 06:19
41423a5

Choose a tag to compare

Minor Changes

  • #20 33e9fb0 Thanks @flyon! - Breaking: QueryParser has been removed. If you imported QueryParser directly, replace with getQueryDispatch() from @_linked/core/queries/queryDispatch. The Shape DSL (Shape.select(), .create(), .update(), .delete()) and SelectQuery.exec() are unchanged.

    New: getQueryDispatch() and setQueryDispatch() are now exported, allowing custom query dispatch implementations (e.g. for testing or alternative storage backends) without subclassing LinkedStorage.

v1.2.1

03 Mar 23:37
d1518ee

Choose a tag to compare

Patch Changes

  • #17 0654780 Thanks @flyon! - Preserve nested array sub-select branches in canonical IR so build() emits complete traversals, projection fields, and resultMap entries for nested selections.

    This fixes cases where nested branches present in toRawInput().select were dropped during desugar/lowering (for example nested friends.select([name, hobby]) branches under another sub-select).

    Also adds regression coverage for desugar preservation, IR lowering completeness, and updated SPARQL golden output for nested query fixtures.

v1.2.0

02 Mar 13:44
9847957

Choose a tag to compare

Minor Changes

  • #9 381067b Thanks @flyon! - Replaced internal query representation with a canonical backend-agnostic IR AST. SelectQuery, CreateQuery, UpdateQuery, and DeleteQuery are now typed IR objects with kind discriminators, compact shape/property ID references, and expression trees — replacing the previous ad-hoc nested arrays. The public Shape DSL is unchanged; what changed is what IQuadStore implementations receive. Store result types (ResultRow, SelectResult, CreateResult, UpdateResult) are now exported. All factories expose build() as the primary method. See documentation/intermediate-representation.md for the full IR reference and migration guidance.

  • #14 b65e156 Thanks @flyon! - Add SPARQL conversion layer — compiles Linked IR queries into executable SPARQL and maps results back to typed DSL objects.

    New exports from @_linked/core/sparql:

    • SparqlStore — abstract base class for SPARQL-backed stores. Extend it and implement two methods to connect any SPARQL 1.1 endpoint:

      import { SparqlStore } from "@_linked/core/sparql";
      
      class MyStore extends SparqlStore {
        protected async executeSparqlSelect(
          sparql: string
        ): Promise<SparqlJsonResults> {
          /* ... */
        }
        protected async executeSparqlUpdate(sparql: string): Promise<void> {
          /* ... */
        }
      }
    • IR → SPARQL string convenience functions (full pipeline in one call):

      • selectToSparql(query, options?) — SelectQuery → SPARQL string
      • createToSparql(query, options?) — CreateQuery → SPARQL string
      • updateToSparql(query, options?) — UpdateQuery → SPARQL string
      • deleteToSparql(query, options?) — DeleteQuery → SPARQL string
    • IR → SPARQL algebra (for stores that want to inspect/optimize the algebra before serialization):

      • selectToAlgebra(query, options?) — returns SparqlSelectPlan
      • createToAlgebra(query, options?) — returns SparqlInsertDataPlan
      • updateToAlgebra(query, options?) — returns SparqlDeleteInsertPlan
      • deleteToAlgebra(query, options?) — returns SparqlDeleteInsertPlan
    • Algebra → SPARQL string serialization:

      • selectPlanToSparql(plan, options?), insertDataPlanToSparql(plan, options?), deleteInsertPlanToSparql(plan, options?), deleteWherePlanToSparql(plan, options?)
      • serializeAlgebraNode(node), serializeExpression(expr), serializeTerm(term)
    • Result mapping (SPARQL JSON results → typed DSL objects):

      • mapSparqlSelectResult(json, query) — handles flat/nested/aggregated results with XSD type coercion
      • mapSparqlCreateResult(uri, query) — echoes created fields with generated URI
      • mapSparqlUpdateResult(query) — echoes updated fields
    • All algebra types re-exported: SparqlTerm, SparqlTriple, SparqlAlgebraNode, SparqlExpression, SparqlSelectPlan, SparqlInsertDataPlan, SparqlDeleteInsertPlan, SparqlDeleteWherePlan, SparqlPlan, SparqlOptions, etc.

    Bug fixes included:

    • Fixed isNodeReference() in MutationQuery.ts — nested creates with predefined IDs (e.g., {id: '...', name: 'Bestie'}) now correctly insert entity data instead of only creating the link.

    See SPARQL Algebra Layer docs for the full type reference, conversion rules, and store implementation guide.