@metreeca/blue - v0.12.0
    Preparing search index...

    @metreeca/blue

    npm

    Declarative blueprints for model-driven linked data processing.

    @metreeca/blue provides a shape-based schema framework for the linked data model defined by @metreeca/qest.

    Shape-based schemas go beyond structural validation, capturing the complete semantics of a resource (structure, constraints, metadata, and relationships), enabling them to act as a single source of truth for automated validation, persistence, API publishing, UI generation, and more:

    • Define Once, Use Everywhere: a single schema drives all automated processes
    • Guaranteed Consistency: schema changes propagate to all dependent processes automatically
    • Less Code to Maintain: declarative definitions replace scattered imperative logic

    @metreeca/blue is designed for a broad range of model-driven tasks and ships with a robust and ready-to-use validation engine:

    • No Type Duplication: types and validation rules derived from a single schema
    • One Schema, Every Mode: same schema validates state, updates, projections, and queries
    • Actionable Error Feedback: per-value, per-property traces surface precise violations
    • Custom When Needed: pluggable validators extend built-in constraints
    Note

    @metreeca/blue is part of the @metreeca/qest integrated ecosystem for rapid development of linked data applications.

    Installation

    npm install @metreeca/blue
    
    Warning

    TypeScript consumers must use "moduleResolution": "nodenext"/"node16"/"bundler" in tsconfig.json. The legacy "node" resolver is not supported.

    Usage

    Note

    This section introduces essential concepts; for complete coverage, see the API reference:

    Module Description
    @metreeca/blue Linked data validation
    @metreeca/blue/value Value shape types and operations
    @metreeca/blue/boolean Boolean shape types and operations
    @metreeca/blue/number Number shape types and operations
    @metreeca/blue/string String shape types and operations
    @metreeca/blue/dictionary Dictionary shape types and operations
    @metreeca/blue/reference Reference shape types and operations
    @metreeca/blue/resource Resource shape types and operations
    @metreeca/blue/union Union shape types and operations

    Schemas describe the expected structure of a resource using shape factories:

    import { union } from "@metreeca/blue/union";
    import { boolean } from "@metreeca/blue/boolean";
    import { number } from "@metreeca/blue/number";
    import { string, url } from "@metreeca/blue/string";
    import { dictionary } from "@metreeca/blue/dictionary";
    import { reference } from "@metreeca/blue/reference";
    import { id, multiple, optional, required, resource, type } from "@metreeca/blue/resource";

    function Thing() {
    return resource({
    id: id(),
    type: type()
    });
    }

    function Product() {
    return resource(Thing, {
    name: required(dictionary()),
    description: optional(dictionary()),
    price: required(number({ minInclusive: 0 })),
    inStock: required(boolean()),
    tags: multiple(string()),
    rating: optional(Rating),
    vendor: required(reference(Vendor))
    });
    }

    function Rating() {
    return resource({
    average: required(number({ minInclusive: 0, maxInclusive: 5 })),
    reviews: required(number({ minInclusive: 1 }))
    });
    }

    function Vendor() {
    return resource(Thing, {
    name: required(string()),
    website: required(url()),
    address: optional(union(
    string(),
    PostalAddress(),
    VirtualLocation()
    ))
    });
    }

    Shape factories like string(), number(), boolean(), dictionary(), and reference() define the expected value type and optional constraints for each property. Cardinality factories wrap a shape into a property, controlling how many values are expected and determining the inferred TypeScript type:

    Factory Cardinality TypeScript Type
    required(s) 1..1 V
    optional(s) 0..1 undefined | V
    nonempty(s) 1..* readonly [V, ...V[]]
    multiple(s) 0..* undefined | readonly V[]
    property(s, { minCount: l, maxCount: u }) l..u as l and u imply

    An upper bound of 1 yields the bare value and any other an array, non-empty where at least one value is required. property() follows the same rules, so bounds beyond the four named cardinalities are typed exactly as their counterparts are.

    A dictionary() range stands apart: a localised property carries its language map whole, never in an array, so it is typed as the bare map at every cardinality. Where a dictionary sits in a union beside other branches, the property is typed as either the map or the array those branches imply, since a resource carries one or the other and never both, and the bounds count the other branches alone.

    Each factory takes the constraints the property carries beyond its cardinality, such as IRI mappings, labels, ownership flags, or a hidden flag withholding it from default serialisation, as a trailing argument: required(string(), { forward: schema }). The id() and type() markers take the same hidden flag.

    Cardinalities admitting absence also relax their key to an optional one, so a value literal spells out only the members it actually carries; reading an omitted member still yields undefined.

    Resource members link to other resources in two ways. A reference() wrapper links to a standalone resource, an independently identified and managed entity like Vendor. A direct shape inclusion defines an embedded resource, a nested object with no independent identity, created and managed together with its parent like Rating.

    Properties accepting values of more than one type are modelled as unions of positional branches. Matching splits by regime: a stored value must single out exactly one branch (sh:xone), tested against all constraints, and is rejected when it fits several (ambiguous) or none (unsatisfiable); a relational bound must likewise single out exactly one, but keys on syntactic traits alone, as it need not be a legal value; a retrieval placeholder is tested by form alone and must fit at least one branch (sh:or), may fit several, and is rejected only when it fits none (see Validating Templates). A multi-valued property matches each of its values independently, and values are stored as they stand with no branch wrapping.

    Branches are expected to be disjoint: overlapping ones are accepted as the shape is built, and an ambiguous value is rejected only when it is matched. Either of the following representations is accepted at the same address position:

    { "address": "12 Harbour Street, Copenhagen" }
    
    {
    "address": {
    "streetAddress": "12 Harbour Street",
    "addressLocality": "Copenhagen"
    }
    }

    Declare parent shapes first to inherit their members and constraints. Local members augment the parent and may override inherited ones, but only by narrowing: an override restricts what it inherits and never relaxes it. Cardinality narrows monotonically (required may override optional, but not the reverse), per-kind constraints intersect, and the override is rejected at the call site when the child relaxes the parent.

    const NamedThing = resource({
    id: id(),
    name: required(string({ minLength: 1 }))
    });

    const Vendor = resource(NamedThing, {
    name: required(string({ minLength: 3, maxLength: 80 })), // narrows minLength
    rating: optional(number({ minInclusive: 0, maxInclusive: 5 }))
    });

    A member holding a nested resource or a reference(...) is refined by re-pointing it at a shape that extends the inherited target. The refining shape declares only what it adds or narrows: it reaches the inherited definition through its own parents, so the parent definition is never restated. A reference(...) target that doesn't extend the inherited one, the inherited target's own parent included, is rejected at the call site; a target deferred to break a definition cycle is held to the same rule once its definition stands, so shapes reaching themselves or each other may be re-pointed just as well.

    const Organization = resource({
    class: "https://schema.org/Organization"
    }, {
    id: id(),
    name: required(string())
    });

    const University = resource(Organization, { class: "https://ec2u.eu/University" }, {
    country: required(string())
    });

    const Unit = resource({
    id: id(),
    unitOf: required(reference(Organization)), // referenced target
    host: required(Organization) // embedded target
    });

    const ResearchUnit = resource(Unit, {
    unitOf: required(reference(University)), // re-pointed at the extending target
    host: required(University)
    });

    Extending the inherited target is what makes the refinement legal for an embedded member: a nested resource value must belong to every class the inherited target declares, whether the refining shape states it in its own right or inherits it, so a standalone shape that merely repeats the inherited members is rejected.

    When the parent declares a union(...) member, an extending shape may narrow it in two forms:

    • Single-branch narrowing: the child supplies a non-union shape that narrows exactly one of the parent's branches. The merged member becomes a bare value shape, no longer polymorphic.
    • Branch subsetting: the child supplies a smaller union(...); each child branch narrows a distinct parent branch (an injective pairing), the paired branches are merged, and unpaired parent branches are dropped.

    A child branch narrowing no parent branch, several, or one already claimed by another child branch is rejected at the call site.

    const Entity = resource({
    code: required(union(string(), number()))
    });

    // Form 1 — narrows the member to a bare string
    const Vendor = resource(Entity, {
    code: required(string({ pattern: /^[A-Z]/ }))
    });

    // Form 2 — keeps the union but drops the string branch wholesale
    const Numbered = resource(Entity, {
    code: required(union(number({ minInclusive: 0 })))
    });

    Dropping a parent branch changes nothing a retrieval template relies on: the keys of a branch map are opaque labels carrying no positional meaning, so a placeholder singles out the alternative it fits by shape rather than by the position the branch was stated at (see Validating Templates).

    Schemas double as TypeScript type definitions. State yields the value a resource carries as it is held:

    import { type State } from "@metreeca/blue/value";

    type ProductType = State<typeof Product>;

    // {
    // readonly id: Reference,
    // readonly type?: undefined | Reference,
    // readonly name: { readonly [tag: Tag]: readonly string[] },
    // readonly description?: undefined | { readonly [tag: Tag]: readonly string[] },
    // readonly price: number,
    // readonly inStock: boolean,
    // readonly tags?: undefined | readonly string[],
    // readonly rating?: undefined | { readonly average: number, readonly reviews: number },
    // readonly vendor: Reference
    // }

    Members admitting absence are optional keys: a value may either set them to undefined or leave them out.

    No separate interface needed: the schema is the type definition.

    Draft yields the state of a resource to be persisted, with its identifier optional, as the target of the operation already identifies the resource or leaves the store to assign it; stated with a collection model, such as Draft<typeof Catalog, { items: {} }>, it yields the state of an item to be added to that collection instead. The blueprint accessor resolves the shape such an item must satisfy, so that it can be validated before it is stored. The collection accessor resolves the property holding the collection.

    Match narrows the value to the members a retrieval template asked for, so a caller reads back its own request rather than everything the schema declares:

    import { type Match } from "@metreeca/blue/value";

    type ProductSummary = Match<typeof Product, { id: {}, price: {} }>;

    // {
    // readonly id: Reference,
    // readonly price: number
    // }

    The template states which values are wanted and no longer what they are, so depth, cardinality and optionality all come from the schema. A member the template leaves out is left out of the result; a polymorphic member, a projection column and a localised member come back as wide as the schema describes them.

    The retrieval models themselves are held to the schema where they are written:

    • Model admits the templates a resource shape serves, rejecting a member the shape doesn't carry
    • Slice admits the models a collection held by a property serves, merged with the criteria filtering, ordering and paginating it
    • Items types the items of such a collection, narrowed to what the model asked for
    • Frame types a single item of such a collection, for code generic over the schema and the model

    The items accessor gets those items from the resource a slice retrieval returns, typed as Items, without looking up the collection property by name.

    The overloaded validate function checks a value against a schema and returns a Relay that dispatches to either a value or trace handler:

    import { validate } from "@metreeca/blue";

    validate(data, { shape: Product })({
    value: product => {
    // product is typed as State<typeof Product>
    },
    trace: trace => {
    // trace describes validation violations
    }
    });

    All constraints are enforced, including type, cardinality, closed-shape checks, and custom validators. Unknown and missing members are both rejected. On success, the value comes back typed as the shape describes it. Validation is idempotent on a given shape: re-validating the same value on the same terms reads the earlier verdict off the value rather than walking it again, so a caller may validate defensively wherever it is unsure.

    Two further options bound what a resource may carry: entry names the identifier the resource is expected to be named by, and depth caps the nesting a captive member may be expanded to, with 0 refusing every expansion while still admitting the identifier naming the resource.

    When the projection template is not bonded to the shape (typically at API boundaries where shape defines the admissible surface and the projection arrives per request), pass model as a separate template argument. Only the members the template asked for are checked, and the result is narrowed to them:

    import { validate } from "@metreeca/blue";

    const model = { id: {}, name: {} }; // projection requested by the caller

    validate(response, { shape: Product, model })({
    value: product => {
    // product is typed as Match<typeof Product, typeof model>
    },
    trace: trace => {
    // trace describes validation violations
    }
    });

    Constraints on members absent from model are not enforced, so an unrequested required member triggers no minCount violation. A member the template didn't ask for is rejected all the same, as the caller has nowhere to put it. A reference member additionally accepts an expanded nested resource, validated against the target shape narrowed by the nested projection in model.

    The same validate function validates retrieval templates when the model option is set to true:

    import { validate } from "@metreeca/blue";

    validate(data, { model: true, shape: Product });
    validate(data, { model: true, shape: Product, plain: true });
    validate(data, { model: true, shape: Product, depth: 0 });
    validate(data, { model: true, shape: Product, limit: 100 });

    A template describes what to retrieve rather than what is held, so what it is held to is asking for something the shape can give: value constraints are left alone, a placeholder carrying no value of its own. A missing member is accepted as not requested, and an explicit undefined member reads the same way, marking one elided at construction time.

    Every leaf is the atomic placeholder {}, and a collection is the entry naming it, carrying the constraints that filter, sort and page it alongside the keys retrieving its values:

    const template = {

    id: {}, // the value as it stands
    vendor: { name: {} }, // a linked resource, expanded

    items: { name: {}, ">=price": 50, "#": 25 }, // a collection, per-item keys and constraints together
    title: { "*": {} } // a localised property, by tag range

    };

    Cardinality is not stated by the notation, so what refuses a constraint is the shape: a member admitting one value has no collection to narrow, and a localised member is filtered by its own tag ranges.

    What each kind of member may be asked for:

    • Reference: either the atomic placeholder, retrieving the identifier naming the target, or a nested template validated against the target shape. An embedded resource, naming no identifier to come back as, is reached through a template alone.
    • Union: the keyed form ({"0": …, "1": …}), one placeholder per alternative wanted, where the alternatives want different shapes; where one shape serves them all, the atomic placeholder addresses the property directly. Keys are opaque labels carrying no positional meaning: each placeholder is matched by form alone and retrieves every branch it fits. The atomic placeholder requests every branch coming back as a value. A nested template reaches every resource branch admitting at least one of the members it asks for, so it may span several, each answering the members it admits. A member several branches declare may take a different shape in each, and what is asked for it need fit only one of them. Only a placeholder fitting no branch at all is rejected. A tag range may also be a member name, so an object whose keys each name a member of a resource branch is read as a template, and any other object as a map of tag ranges.
    • Localised: a map of the tag ranges wanted, each asking for the value a matched tag carries, or the atomic placeholder, standing for the content language negotiation settles on. Per-tag arity follows the shape rather than the template. A localised branch of a union takes the map within a projection column alone, where each branch is asked for under a column of its own; elsewhere it comes back coalesced.

    Constraint operands follow their own rules: comparison bounds and set-matching options carry content rather than placeholders, so each must single out exactly one branch, a bound keying on kind and lexical pattern and an option on kind alone, while a ~ text search is a plain string applied to every string branch at once. A plain-string operand over a localised member filters the negotiated content under ordinary textual semantics; sorting and focusing still require a single-valued key, so they accept a coalesced localised key only where it resolves single-valued.

    Three options bound the query language a template may draw on:

    • plain: rejects the aggregate transforms combining several values into one (count, sum, min, max, avg)
    • depth: caps nested template expansion and property path length
    • limit: caps the # pagination constraint, and is injected as a default where a collection states none; 0, like omitting it, leaves the page to the client
    Caution

    By default, templates support the full query language, including aggregate transforms and nested expansion. When exposing endpoints to untrusted clients, restrict query complexity as required by setting plain to true, depth to 0 or a positive value, and/or limit to a maximum result set size.

    SHACL Foundations

    SHACL (Shapes Constraint Language) is a W3C standard for describing and validating RDF graphs. It defines shapes (sets of constraints that nodes in a graph must satisfy) covering structure, cardinality, value ranges, and logical combinations.

    @metreeca/blue implements a controlled SHACL subset tailored to the JSON-LD profile defined by @metreeca/qest, enabling TypeScript developers to use shape-based validation without mastering SHACL technicalities.

    This controlled subset is specified by:

    • cardinality constraints (sh:minCount, sh:maxCount) for specifying how many values a property must or may have
    • value range constraints (sh:minExclusive, sh:maxExclusive, sh:minInclusive, sh:maxInclusive) for numeric value ranges
    • string constraints (sh:minLength, sh:maxLength, sh:pattern, sh:languageIn, sh:uniqueLang) for text length, patterns, and language tags
    • value type constraints (sh:class, sh:datatype) for declaring the expected type of resource instances and the RDF datatype of literals; sh:class is limited to a single class
    • value constraints (sh:in, sh:hasValue) for enumerations and required values
    • logical constraints limited to sh:xone typed unions on members, matched exactly-one on write and relaxed to at-least-one (sh:or) on read; the sh:not, sh:and, and sh:or shape combinators are not supported for authoring
    • closed shapes enforced by default on all resource shapes; unknown properties are always rejected

    Property pair constraints and property paths are not supported; cross-property logic can be implemented via custom validators.

    Support

    • Open an issue to report a problem or to suggest a new feature
    • Start a discussion to ask a how-to question or to share an idea

    License

    This project is licensed under the Apache 2.0 License – see LICENSE file for details.