npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@spectragraph/core

v0.7.0

Published

A JavaScript library for working with schema-driven graph data. SpectraGraph provides powerful tools for querying, validating, and manipulating relational data structures with a focus on type safety and developer experience. It is intended to be a toolkit

Readme

SpectraGraph Core

A JavaScript library for working with schema-driven graph data. SpectraGraph provides powerful tools for querying, validating, and manipulating relational data structures with a focus on type safety and developer experience. It is intended to be a toolkit for building SpectraGraph stores.

Overview

SpectraGraph Core is built around several key principles:

  • Schema-driven: Everything flows from user-defined schemas that describe your data structure
  • Query-focused: Powerful query language for retrieving exactly the data you need
  • Practical: Optimized for real-world messy data scenarios with schema powered validation

Installation

npm install @spectragraph/core

Core Concepts

Schemas

Schemas define the structure of your data, including resource types, attributes, and relationships. They serve as the foundation for all SpectraGraph operations.

const schema = {
  resources: {
    teams: {
      attributes: {
        id: { type: "string" },
        name: { type: "string" },
        city: { type: "string" },
      },
      relationships: {
        homeMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "homeTeam",
        },
        awayMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "awayTeam",
        },
      },
    },
    matches: {
      attributes: {
        id: { type: "string" },
        field: { type: "string" },
        ageGroup: { type: "integer" },
      },
      relationships: {
        homeTeam: { type: "teams", cardinality: "one", inverse: "homeMatches" },
        awayTeam: { type: "teams", cardinality: "one", inverse: "awayMatches" },
      },
    },
  },
};

Graphs

Graphs represent your actual data in a normalized structure, organized by resource type and ID:

const graph = {
  teams: {
    "team-1": {
      type: "teams",
      id: "team-1",
      attributes: { name: "Arizona Bay FC", city: "Phoenix" },
      relationships: {
        homeMatches: [{ type: "matches", id: "match-1" }],
        awayMatches: [{ type: "matches", id: "match-2" }],
      },
    },
    "team-2": {
      type: "teams",
      id: "team-2",
      attributes: { name: "Scottsdale Surf", city: "Scottsdale" },
      relationships: {
        homeMatches: [{ type: "matches", id: "match-2" }],
        awayMatches: [{ type: "matches", id: "match-1" }],
      },
    },
  },
  matches: {
    "match-1": {
      type: "matches",
      id: "match-1",
      attributes: { field: "Phoenix Park 1", ageGroup: 11 },
      relationships: {
        homeTeam: { type: "teams", id: "team-1" },
        awayTeam: { type: "teams", id: "team-2" },
      },
    },
    "match-2": {
      type: "matches",
      id: "match-2",
      attributes: { field: "Scottsdale Community Center", ageGroup: 11 },
      relationships: {
        homeTeam: { type: "teams", id: "team-2" },
        awayTeam: { type: "teams", id: "team-1" },
      },
    },
  },
};

Queries

Queries describe what data you want to retrieve, closely matching your desired output structure:

const query = {
  type: "teams",
  select: ["name", { homeMatches: ["field", "ageGroup"] }],
};

Grouping and Aggregation

SpectraGraph supports grouping and aggregation operations through the group clause, enabling analytics queries similar to SQL's GROUP BY functionality.

Grand Totals

Aggregate all resources into a single group using an empty by array:

// Compute totals across all matches
const query = {
  type: "matches",
  group: {
    by: [],
    aggregates: {
      totalMatches: { $count: null },
      totalGoals: { $sum: { $pluck: "goals" } },
      avgGoals: { $mean: { $pluck: "goals" } },
    },
  },
};
// Returns: [{ totalMatches: 150, totalGoals: 423, avgGoals: 2.82 }]

This is equivalent to SQL aggregates without a GROUP BY clause.

Basic Grouping

Group resources by one or more attributes:

// Group matches by age group
const query = {
  type: "matches",
  group: {
    by: "ageGroup",
  },
};
// Returns: [{ ageGroup: 11 }, { ageGroup: 12 }, { ageGroup: 13 }]

// Group by multiple fields
const query = {
  type: "matches",
  group: {
    by: ["ageGroup", "field"],
  },
};

Aggregates

Compute aggregate values for each group:

// Count matches per age group
const query = {
  type: "matches",
  group: {
    by: "ageGroup",
    aggregates: {
      total: { $count: null },
    },
  },
};
// Returns: [
//   { ageGroup: 11, total: 2 },
//   { ageGroup: 12, total: 1 },
//   { ageGroup: 13, total: 1 }
// ]

// Sum goals per age group
const query = {
  type: "matches",
  group: {
    by: "ageGroup",
    aggregates: {
      totalGoals: { $sum: { $pluck: "goals" } },
    },
  },
};

Select in Groups

Control which fields to include in results:

// Rename fields
const query = {
  type: "matches",
  group: {
    by: "ageGroup",
    select: { age: "ageGroup" }, // Rename ageGroup to age
  },
};

// Compute derived fields
const query = {
  type: "matches",
  group: {
    by: "ageGroup",
    select: [
      "ageGroup",
      {
        tier: {
          $if: {
            if: { $gte: [{ $get: "ageGroup" }, 12] },
            then: "senior",
            else: "junior",
          },
        },
      },
    ],
  },
};

Filtering Within Groups

Use where on a group clause to filter groups after aggregation (similar to SQL's HAVING):

// Only show age groups with more than 2 total goals
const query = {
  type: "matches",
  group: {
    by: "ageGroup",
    aggregates: { total: { $sum: { $pluck: "goals" } } },
    where: { $gt: [{ $get: "total" }, 2] },
  },
};

Ordering, Limiting Groups

Groups support the same query modifiers as regular queries:

const query = {
  type: "matches",
  group: {
    by: "ageGroup",
    aggregates: { total: { $sum: { $pluck: "goals" } } },
    order: { total: "desc" }, // Order by aggregate
    limit: 5,
    offset: 0,
  },
};

Nested Grouping

Groups can be nested to create multi-level aggregations:

// Group by age, compute tier, then regroup by tier
const query = {
  type: "matches",
  group: {
    by: "ageGroup",
    select: [
      "ageGroup",
      {
        tier: {
          $if: {
            if: { $gte: [{ $get: "ageGroup" }, 12] },
            then: "senior",
            else: "junior",
          },
        },
      },
    ],
    group: {
      by: "tier",
      aggregates: { count: { $count: null } },
    },
  },
};
// Returns: [
//   { tier: "junior", count: 1 },
//   { tier: "senior", count: 2 }
// ]

Expressions

SpectraGraph queries support expressions for computed fields and conditional logic. These expressions are provided by the json-expressions library, which offers a comprehensive set of operators for data transformation and filtering.

Expression Engines

SpectraGraph uses focused expression engines to provide different capabilities for different query contexts:

  • SELECT Engine (defaultSelectEngine): Full expression capabilities including filtering, aggregations, transformations, and computed fields
  • WHERE Engine (defaultWhereEngine): Filtering-only operations for performance and security - excludes expensive aggregation operations

This architecture ensures that resource-intensive operations are only available where appropriate, preventing performance issues from expensive aggregations in WHERE clauses.

What are Expressions?

Expressions are JSON objects that describe computations to be performed on data. Each expression has a single key that identifies the operation (prefixed with $) and a value that provides the parameters:

Simple comparison:

{ "$gt": 11 }

Logical expressions:

{ "$and": [{ "$gte": 2020 }, { "$eq": "active" }] }

Conditional logic:

{
  "$if": {
    "if": { "$eq": "Phoenix" },
    "then": { "$get": "name" },
    "else": "Away Team"
  }
}

Basic Expression Examples

Check if player is eligible for age group:

{ "$gte": 11 }

Get team's home field:

{ "$get": "homeField.name" }

Calculate total match cost:

{ "$add": [150, 25] }

Sum all team scores:

{ "$sum": [2, 1, 4, 0] }

Filter teams by city:

{ "$filter": { "$eq": "Phoenix" } }

Get all team names:

{ "$map": { "$get": "name" } }

Join team cities:

{ "$join": " vs " }

Available Operations

The json-expressions library provides comprehensive operations including:

  • Core: $compose, $debug, $ensurePath, $get, $prop, $isPresent, $literal, $pipe
  • Conditionals: $switch, $case, $if
  • Logic: $and, $not, $or
  • Comparisons: $eq, $gt, $gte, $lt, $lte, $in, $nin, $ne
  • Pattern matching: $matchesRegex, $matchesLike, $matchesGlob
  • Aggregation: $count, $max, $min, $mean, $median, $mode, $sum
  • Array operations: $all, $any, $append, $filter, $find, $flatMap, $join, $map, $prepend, $reverse
  • Math: $add, $subtract, $multiply, $divide, $modulo
  • Generative: $random, $uuid
  • Temporal: $nowLocal, $nowUTC, $timestamp

For complete documentation of all available operations, see the json-expressions documentation.

Custom Expressions

Available expressions can be extended. Most stores accept selectEngine and whereEngine parameters. See the json-expressions documentation for how to create and use custom expressions.

Using Expressions in Queries

Expressions can be used in both where clauses for filtering and select clauses for computed fields:

// Query teams with expressions in where clause
const teamQuery = {
  type: "teams",
  where: {
    $and: [
      { founded: { $gte: 2000 } }, // founded >= 2000
      { founded: { $lte: 2020 } }, // founded <= 2020
      { active: { $eq: true } }, // active === true
    ],
  },
  select: {
    name: "name",
    city: "city",
    seasonsActive: { $subtract: [2024, { $get: "founded" }] }, // Computed field
    division: "division",
  },
};

// Complex conditional logic for match analysis
const matchQuery = {
  type: "matches",
  select: {
    field: "field",
    ageGroup: "ageGroup",
    status: {
      $case: {
        value: { $get: "ageGroup" },
        cases: [
          { when: { $lt: 12 }, then: "Youth League" },
          { when: { $lt: 16 }, then: "Junior League" },
          { when: { $gte: 16 }, then: "Senior League" },
        ],
        default: "Unassigned",
      },
    },
    isPlayoff: {
      $and: [
        { $gte: 10 }, // week >= 10
        { $eq: "important" }, // importance === "important"
      ],
    },
  },
};

API Reference

Schema Functions

ensureValidSchema(schema, options?)

Validates that a schema conforms to the SpectraGraph schema specification.

Parameters:

  • schema (unknown) - The schema to validate
  • options.validator (Ajv, optional) - Custom AJV validator instance

Throws: Error if schema is invalid

import { ensureValidSchema } from "@spectragraph/core";

ensureValidSchema(mySchema); // Throws if invalid

Graph Functions

createEmptyGraph(schema, options?)

Creates an empty graph structure based on a schema.

Parameters:

  • schema (Schema) - The schema to base the graph on
  • options.skipValidation (boolean, optional) - Skip schema validation for performance

Returns: Empty graph with resource type placeholders

import { createEmptyGraph } from "@spectragraph/core";

const emptyGraph = createEmptyGraph(schema);
// Returns: { teams: {}, matches: {} }

// Skip validation for performance
const emptyGraph = createEmptyGraph(schema, { skipValidation: true });

linkInverses(schema, graph)

Links inverse relationships in a graph, filling in missing relationship data where possible. Queries will not work without relationships going in the direction of the query, so it's important to use this if you're constructing a graph from semi-connected pieces. This is commonly used with mergeGraphsDeep.

Parameters:

  • schema (Schema) - The schema defining relationships
  • graph (Graph) - The graph to link inverses in

Returns: New graph with inverse relationships linked

import {
  linkInverses,
  buildNormalResource,
  createEmptyGraph,
} from "@spectragraph/core";

// Example: Multi-API store pattern
const graph = createEmptyGraph(schema);

// Resources built with includeRelationships: false have undefined relationships
const team = buildNormalResource(schema, "teams", apiData, {
  includeRelationships: false,
});
graph.teams["1"] = team;

// Link relationships from their inverses
const linkedGraph = linkInverses(schema, graph);

mergeGraphs(left, right)

Merges two graphs together by combining resource collections. Right graph takes precedence for resources with conflicting IDs.

Parameters:

  • left (Graph) - The left graph
  • right (Graph) - The right graph (takes precedence for conflicts)

Returns: Merged graph with combined resource collections

import { mergeGraphs } from "@spectragraph/core";

const combined = mergeGraphs(graph1, graph2);

mergeGraphsDeep(left, right)

Merges two graphs together, merging individual resources with matching IDs using mergeNormalResources().

Parameters:

  • left (Graph) - The left graph
  • right (Graph) - The right graph

Returns: Merged graph with resources intelligently merged

import { mergeGraphsDeep } from "@spectragraph/core";

const combined = mergeGraphsDeep(graph1, graph2);

Query Functions

ensureValidQuery(schema, query, options?)

Validates that a query is valid against a schema.

Parameters:

  • schema (Schema) - The schema to validate against
  • query (RootQuery) - The query to validate
  • options.selectEngine (SelectExpressionEngine, optional) - Expression engine for SELECT clauses
  • options.whereEngine (WhereExpressionEngine, optional) - Expression engine for WHERE clauses

Throws: Error if query is invalid

import { ensureValidQuery } from "@spectragraph/core";

ensureValidQuery(schema, query); // Uses default engines

normalizeQuery(schema, rootQuery, options?)

Normalizes a query by expanding shorthand syntax and ensuring consistent structure.

Parameters:

  • schema (Schema) - The schema object
  • rootQuery (RootQuery) - The query to normalize
  • options.selectEngine (SelectExpressionEngine, optional) - Expression engine for SELECT clauses
  • options.whereEngine (WhereExpressionEngine, optional) - Expression engine for WHERE clauses

Returns: Normalized query with expanded selections and consistent structure

import { normalizeQuery } from "@spectragraph/core";

const normalized = normalizeQuery(schema, {
  type: "users",
  select: "*", // Expands to all attributes
});

getQueryExtentByClause(schema, query)

Analyzes a query to determine which schema attributes and relationships are referenced by each query clause (select, where, order, group) separately.

This enables fine-grained optimization by store implementations where different clauses have different performance or security characteristics.

Parameters:

  • schema (Schema) - The schema defining resource types and relationships
  • query (RootQuery) - The query object to analyze

Returns: Object with separate extents for each clause:

{
  select: QueryExtent,
  where: QueryExtent,
  order: QueryExtent,
  group: QueryExtent
}

Where QueryExtent has the structure:

{
  attributes: string[],
  relationships: { [relationshipName: string]: QueryExtent }
}

Use Cases:

  • SQL query building: WHERE clause paths → JOIN/WHERE, SELECT paths → SELECT columns, ORDER paths → ORDER BY
  • Access control: Apply different permissions to filter fields vs display fields
  • Incremental fetching: Fetch filtering data first, then display data for filtered results
  • Query optimization: Identify which fields need indexes (ORDER/WHERE) vs which are just displayed
import { getQueryExtentByClause } from "@spectragraph/core";

const extents = getQueryExtentByClause(schema, {
  type: "bears",
  select: ["name", { home: ["caringMeter"] }],
  where: { furColor: "brown" },
  order: { yearIntroduced: "asc" },
});

// Returns:
// {
//   select: {
//     attributes: ["name"],
//     relationships: { home: { attributes: ["caringMeter"], relationships: {} } }
//   },
//   where: {
//     attributes: ["furColor"],
//     relationships: {}
//   },
//   order: {
//     attributes: ["yearIntroduced"],
//     relationships: {}
//   },
//   group: {
//     attributes: [],
//     relationships: {}
//   }
// }

// Use in SQL store:
const whereColumns = extents.where.attributes; // ["furColor"]
const selectColumns = extents.select.attributes; // ["name"]
const orderColumns = extents.order.attributes; // ["yearIntroduced"]
const joins = Object.keys(extents.select.relationships); // ["home"]

getFullQueryExtent(schema, query)

Analyzes a query to determine the complete set of schema attributes and relationships referenced across all query clauses (select, where, order, group), merged into a single extent.

This is useful for store implementations that need to know all data requirements upfront.

Parameters:

  • schema (Schema) - The schema defining resource types and relationships
  • query (RootQuery) - The query object to analyze

Returns: Single QueryExtent object with all referenced attributes and relationships merged

Use Cases:

  • Prefetching: Fetch all needed data in one round trip
  • Simple permission checks: Does user have access to ANY of these fields?
  • Caching: Cache key based on all accessed data
  • Multi-API coordination: Coordinate fetches across multiple backend stores
import { getFullQueryExtent } from "@spectragraph/core";

const extent = getFullQueryExtent(schema, {
  type: "bears",
  select: ["name", { home: ["caringMeter"] }],
  where: { furColor: "brown" },
  order: { yearIntroduced: "asc" },
});

// Returns:
// {
//   attributes: ["name", "furColor", "yearIntroduced"],
//   relationships: {
//     home: { attributes: ["caringMeter"], relationships: {} }
//   }
// }

// Use in multi-API store:
const allAttributes = extent.attributes; // ["name", "furColor", "yearIntroduced"]
const allRelationships = Object.keys(extent.relationships); // ["home"]
prefetchData(allAttributes, allRelationships);

queryGraph(schema, query, graph, options?)

Executes a query against a graph directly (convenience function).

Parameters:

  • schema (Schema) - The schema defining relationships
  • query (RootQuery) - The query to execute
  • graph (Graph) - The graph to query
  • options.selectEngine (SelectExpressionEngine, optional) - Expression engine for SELECT clauses
  • options.whereEngine (WhereExpressionEngine, optional) - Expression engine for WHERE clauses

Returns: Query results matching the query structure

Query Options:

  • id - Fetch a single resource by ID (returns single object or null)
  • ids - Fetch multiple specific resources by IDs (returns array, mutually exclusive with id)
  • where - Filter resources by conditions
  • order - Sort results
  • limit/offset - Paginate results
import { queryGraph } from "@spectragraph/core";

const results = queryGraph(
  schema,
  {
    type: "teams",
    id: "team-1",
    select: {
      name: "name",
      homeMatches: { select: ["field"] },
    },
  },
  graph,
);

Resource Functions

normalizeResource(schema, resourceType, resource)

Converts a flat resource object into SpectraGraph's normalized format.

Parameters:

  • schema (Schema) - The schema defining the resource structure
  • resourceType (string) - The type of resource being normalized
  • resource (object) - The flat resource data

Returns: Normalized resource with separated attributes and relationships

import { normalizeResource } from "@spectragraph/core";

const flatTeam = {
  id: "team-1",
  name: "Tempe Tidal Wave",
  city: "Tempe",
  homeMatches: ["match-1", "match-3"], // IDs or objects
  awayMatches: ["match-2", "match-4"],
};

const normalized = normalizeResource(schema, "teams", flatTeam);
// Returns normalized resource with attributes and relationships separated

buildResource(schema, resourceType, partialResource, options?)

Creates a flat resource with schema defaults applied to attributes. Returns a flat object with attributes and relationship IDs at the root level.

Parameters:

  • schema (Schema) - The schema defining the resource structure
  • resourceType (string) - The type of resource to build
  • partialResource (object) - The partial resource data
  • options (object, optional) - Configuration options
    • includeRelationships (boolean) - Whether to include default values for relationships not present in partialResource. When false, only relationships explicitly provided in partialResource will be included. Defaults to true.

Returns: Flat resource with attribute defaults applied

import { buildResource } from "@spectragraph/core";

// Default behavior - includes relationship defaults
const team = buildResource(schema, "teams", {
  name: "Phoenix Fire",
  city: "Phoenix",
});
// Returns: { name: "Phoenix Fire", city: "Phoenix", homeMatches: [], awayMatches: [], ...other defaults }

// For Multi-API stores - omit relationship defaults
const teamFromAPI = buildResource(
  schema,
  "teams",
  {
    name: "Phoenix Fire",
    city: "Phoenix",
  },
  { includeRelationships: false },
);
// Returns: { name: "Phoenix Fire", city: "Phoenix", ...attribute defaults only }

buildNormalResource(schema, resourceType, partialResource, options?)

Creates a normalized resource with schema defaults applied to attributes. Returns a resource with the type/id/attributes/relationships structure.

Parameters:

  • schema (Schema) - The schema defining the resource structure
  • resourceType (string) - The type of resource to build
  • partialResource (object) - The partial resource data
  • options (object, optional) - Configuration options
    • includeRelationships (boolean) - Whether to include default values for relationships not present in partialResource. When false, only relationships explicitly provided in partialResource will be included. This is useful when relationships will be linked later via linkInverses(). Defaults to true.

Returns: Normalized resource with attribute defaults applied

import { buildNormalResource } from "@spectragraph/core";

// Default behavior - includes relationship defaults
const team = buildNormalResource(schema, "teams", {
  name: "Phoenix Fire",
  city: "Phoenix",
});
// Returns:
// {
//   type: "teams",
//   id: undefined,
//   attributes: { name: "Phoenix Fire", city: "Phoenix", ...defaults },
//   relationships: { homeMatches: [], awayMatches: [] }
// }

// For Multi-API stores - omit relationship defaults
const teamFromAPI = buildNormalResource(
  schema,
  "teams",
  {
    name: "Phoenix Fire",
    city: "Phoenix",
  },
  { includeRelationships: false },
);
// teamFromAPI.relationships = {}

// Explicit relationships are preserved even with includeRelationships: false
const teamWithExplicitRel = buildNormalResource(
  schema,
  "teams",
  {
    name: "Phoenix Fire",
    homeMatches: [{ type: "matches", id: "match-1" }],
  },
  { includeRelationships: false },
);
// teamWithExplicitRel.relationships = { homeMatches: [{ type: "matches", id: "match-1" }] }

mergeNormalResources(left, right)

Merges two partial resources of the same type, combining their attributes and relationships. The right resource takes precedence for conflicting attribute keys.

Parameters:

  • left (PartialNormalResource) - The first resource to merge
  • right (PartialNormalResource) - The second resource to merge

Returns: Merged PartialNormalResource with combined attributes and relationships

Throws: Error if resources are of different types or have conflicting IDs

import { mergeNormalResources } from "@spectragraph/core";

const existing = {
  type: "teams",
  id: "team-1",
  attributes: { name: "Old Name", founded: 2000 },
  relationships: { homeField: { type: "fields", id: "field-1" } },
};

const update = {
  type: "teams",
  id: "team-1",
  attributes: { name: "New Name", active: true }, // name will override
};

const merged = mergeNormalResources(existing, update);
// Returns:
// {
//   type: "teams",
//   id: "team-1",
//   attributes: { name: "New Name", founded: 2000, active: true },
//   relationships: { homeField: { type: "fields", id: "field-1" } }
// }

createGraphFromResources(schema, rootResourceType, rootResources)

Creates a complete graph from an array of resource objects, recursively processing nested relationships. This function is particularly useful for handling nested resources.

Parameters:

  • schema (Schema) - The schema defining the resource structure
  • rootResourceType (string) - The root resource type to process
  • rootResources (Array) - Array of flat resource objects to convert

Returns: Complete graph with all discoverable resources from the input data

import { createGraphFromResources } from "@spectragraph/core";

const flatTeam = {
  id: "team-1",
  name: "Arizona Bay FC",
  city: "Phoenix",
  homeMatches: [
    {
      id: "match-1",
      field: "Phoenix Park 1",
      ageGroup: 11,
      awayTeam: "team-2",
    },
  ],
};

const graph = createGraphFromResources(schema, "teams", [flatTeam]);
// Returns a graph containing teams and matches resources
// with proper normalization and relationship structure

Error Classes

SpectraGraph Core provides specialized error classes to help stores and applications handle unsupported functionality gracefully.

ExpressionNotSupportedError

Error thrown when a store does not support a particular expression. This allows stores to explicitly declare unsupported functionality and enables tests to handle these cases gracefully.

Constructor Parameters:

  • expression (string) - The expression that is not supported (e.g., "$matchesRegex")
  • storeName (string) - The name of the store that doesn't support the expression
  • reason (string, optional) - Optional reason why the expression is not supported

Properties:

  • name - "ExpressionNotSupportedError"
  • expression - The unsupported expression name
  • storeName - The store name
  • reason - Optional reason (if provided)
import { ExpressionNotSupportedError } from "@spectragraph/core";

// In a store implementation
if (!this.supportsRegex) {
  throw new ExpressionNotSupportedError(
    "$matchesRegex",
    "simple-store",
    "Regex operations require additional dependencies",
  );
}

// In application code
try {
  const results = await store.query(queryWithRegex);
} catch (error) {
  if (error instanceof ExpressionNotSupportedError) {
    console.log(
      `Expression ${error.expression} not supported by ${error.storeName}`,
    );
    // Handle gracefully, perhaps with a fallback query
  }
}

StoreOperationNotSupportedError

Error thrown when a store does not support a particular operation. This allows stores to explicitly declare unsupported functionality and enables tests to handle these cases gracefully. This is particularly useful for readonly stores or stores with limited write capabilities.

Constructor Parameters:

  • operation (string) - The store operation that is not supported (e.g., "create", "update", "delete")
  • storeName (string) - The name of the store that doesn't support the operation
  • reason (string, optional) - Optional reason why the operation is not supported

Properties:

  • name - "StoreOperationNotSupportedError"
  • operation - The unsupported operation name
  • storeName - The store name
  • reason - Optional reason (if provided)
import { StoreOperationNotSupportedError } from "@spectragraph/core";

// In a readonly store implementation
class ReadOnlyStore {
  create(resource) {
    throw new StoreOperationNotSupportedError(
      "create",
      "readonly-store",
      "This store only supports read operations",
    );
  }

  update(resource) {
    throw new StoreOperationNotSupportedError("update", "readonly-store");
  }

  delete(resource) {
    throw new StoreOperationNotSupportedError("delete", "readonly-store");
  }
}

// In application code
try {
  await store.create(newResource);
} catch (error) {
  if (error instanceof StoreOperationNotSupportedError) {
    console.log(
      `Operation ${error.operation} not supported by ${error.storeName}`,
    );
    // Handle gracefully, perhaps by using a different store or informing the user
  }
}

Validation Functions

validateSchema(schema, options?)

Validates that a schema conforms to the SpectraGraph schema specification.

Parameters:

  • schema (unknown) - The schema to validate
  • options.validator (Ajv, optional) - Custom AJV validator instance

Returns: Array of validation errors (empty if valid)

import { validateSchema } from "@spectragraph/core";

const errors = validateSchema(mySchema);
if (errors.length > 0) {
  console.error("Schema validation failed:", errors);
}

validateQuery(schema, query, options?)

Validates that a query is valid against a schema.

Parameters:

  • schema (Schema) - The schema to validate against
  • query (RootQuery) - The query to validate
  • options.selectEngine (SelectExpressionEngine, optional) - Expression engine for SELECT clauses
  • options.whereEngine (WhereExpressionEngine, optional) - Expression engine for WHERE clauses

Returns: Array of validation errors (empty if valid)

import { validateQuery } from "@spectragraph/core";

const errors = validateQuery(schema, query);
if (errors.length > 0) {
  console.error("Query validation failed:", errors);
}

createValidator(options?)

Creates a new AJV validator instance configured for SpectraGraph.

Parameters:

  • options.ajvSchemas (Array, optional) - Additional schemas to add

Returns: Configured AJV validator

import { createValidator } from "@spectragraph/core";

const validator = createValidator({
  ajvSchemas: [myCustomSchema],
});

defaultValidator

The default AJV validator instance used by SpectraGraph.

import { defaultValidator } from "@spectragraph/core";

// Use the default validator directly
const isValid = defaultValidator.validate(schema, data);

defaultSelectEngine

The default expression engine for SELECT clauses, providing full expression capabilities including filtering, aggregations, transformations, and computed fields.

import { defaultSelectEngine } from "@spectragraph/core";

// Use in custom query normalization
const normalizedQuery = normalizeQuery(schema, query, {
  selectEngine: defaultSelectEngine,
});

defaultWhereEngine

The default expression engine for WHERE clauses, providing filtering-only operations for performance and security. Excludes expensive aggregation operations.

import { defaultWhereEngine } from "@spectragraph/core";

// Use in custom query validation
const errors = validateQuery(schema, query, {
  whereEngine: defaultWhereEngine,
});

validateCreateResource(schema, resource, options?)

Validates a resource for creation operations.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (CreateResource) - The resource to validate
  • options.validator (Ajv, optional) - Custom validator instance

Returns: Array of validation errors (empty if valid)

import { validateCreateResource } from "@spectragraph/core";

const errors = validateCreateResource(schema, {
  type: "teams",
  attributes: { name: "Scottsdale Surf", city: "Scottsdale" },
});

validateUpdateResource(schema, resource, options?)

Validates a resource for update operations.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (UpdateResource) - The resource to validate
  • options.validator (Ajv, optional) - Custom validator instance

Returns: Array of validation errors (empty if valid)

validateDeleteResource(schema, resource)

Validates a resource for delete operations.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (DeleteResource) - The resource reference to validate

Returns: Array of validation errors (empty if valid)

validateMergeResource(schema, resource, options?)

Validates a resource tree that will be merged into a graph.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (unknown) - The resource tree to validate
  • options.validator (Ajv, optional) - Custom validator instance

Returns: Array of validation errors (empty if valid)

validateQueryResult(schema, rootQuery, result, options?)

Validates that query results match the expected structure.

Parameters:

  • schema (Schema) - The schema to validate against
  • rootQuery (RootQuery) - The original query
  • result (unknown) - The query results to validate
  • options.selectEngine (SelectExpressionEngine, optional) - Expression engine for SELECT clauses
  • options.validator (Ajv, optional) - Custom validator instance

Returns: Array of validation errors (empty if valid)

import { validateQueryResult } from "@spectragraph/core";

const errors = validateQueryResult(schema, query, results);
if (errors.length > 0) {
  console.error("Query result validation failed:", errors);
}

Ensure Functions

The following functions provide throwing variants of the validation functions above, useful for fail-fast scenarios:

ensureValidCreateResource(schema, resource, options?)

Validates a resource for creation operations. Throws on validation failure.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (CreateResource) - The resource to validate
  • options.validator (Ajv, optional) - Custom validator instance

Throws: Error if resource is invalid

import { ensureValidCreateResource } from "@spectragraph/core";

ensureValidCreateResource(schema, newResource); // Throws if invalid

ensureValidUpdateResource(schema, resource, options?)

Validates a resource for update operations. Throws on validation failure.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (UpdateResource) - The resource to validate
  • options.validator (Ajv, optional) - Custom validator instance

Throws: Error if resource is invalid

ensureValidDeleteResource(schema, resource)

Validates a resource for delete operations. Throws on validation failure.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (DeleteResource) - The resource reference to validate

Throws: Error if resource is invalid

ensureValidMergeResource(schema, resource, options?)

Validates a resource tree that will be merged into a graph. Throws on validation failure.

Parameters:

  • schema (Schema) - The schema to validate against
  • resource (unknown) - The resource tree to validate
  • options.validator (Ajv, optional) - Custom validator instance

Throws: Error if resource is invalid

ensureValidQueryResult(schema, rootQuery, result, options?)

Validates that query results match the expected structure. Throws on validation failure.

Parameters:

  • schema (Schema) - The schema to validate against
  • rootQuery (RootQuery) - The original query
  • result (unknown) - The query results to validate
  • options.selectEngine (SelectExpressionEngine, optional) - Expression engine for SELECT clauses
  • options.validator (Ajv, optional) - Custom validator instance

Throws: Error if results don't match expected structure

Examples

Basic Usage

import {
  ensureValidSchema,
  createEmptyGraph,
  queryGraph,
  normalizeResource,
} from "@spectragraph/core";

// 1. Define your schema
const schema = {
  resources: {
    teams: {
      attributes: {
        id: { type: "string" },
        name: { type: "string" },
      },
      relationships: {
        homeMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "homeTeam",
        },
        awayMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "awayTeam",
        },
      },
    },
  },
};

// 2. Validate the schema
ensureValidSchema(schema);

// 3. Create and populate a graph
const graph = createEmptyGraph(schema);
const teamData = { id: "1", name: "Arizona Bay FC" };
const normalizedTeam = normalizeResource(schema, "teams", teamData);

graph.teams["1"] = normalizedTeam;

// 4. Query the data
const results = queryGraph(
  schema,
  {
    type: "teams",
    select: ["name"],
  },
  graph,
);

console.log(results); // [{ name: "Arizona Bay FC" }]

Working with Relationships

const schema = {
  resources: {
    teams: {
      attributes: {
        id: { type: "string" },
        name: { type: "string" },
      },
      relationships: {
        homeMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "homeTeam",
        },
        awayMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "awayTeam",
        },
      },
    },
    matches: {
      attributes: {
        id: { type: "string" },
        field: { type: "string" },
      },
      relationships: {
        homeTeam: { type: "teams", cardinality: "one", inverse: "homeMatches" },
        awayTeam: { type: "teams", cardinality: "one", inverse: "awayMatches" },
      },
    },
  },
};

// Query with relationship traversal
const results = queryGraph(
  schema,
  {
    type: "teams",
    select: {
      name: "name",
      homeMatches: {
        select: ["field", "awayTeam"],
      },
    },
  },
  graph,
);

Data Validation

import {
  validateCreateResource,
  ensureValidCreateResource,
} from "@spectragraph/core";

const newTeam = {
  type: "teams",
  attributes: {
    name: "Mesa Mariners",
    city: "Mesa",
  },
};

// Option 1: Check errors manually
const errors = validateCreateResource(schema, newTeam);
if (errors.length > 0) {
  console.error("Validation failed:", errors);
}

// Option 2: Throw on validation failure
try {
  ensureValidCreateResource(schema, newTeam);
  console.log("Resource is valid!");
} catch (error) {
  console.error("Validation failed:", error.message);
}

TypeScript Support

SpectraGraph Core includes comprehensive TypeScript definitions. Import types as needed:

import type {
  Schema,
  Graph,
  Query,
  RootQuery,
  NormalResource,
} from "@spectragraph/core";

const schema: Schema = {
  resources: {
    teams: {
      attributes: {
        id: { type: "string" },
        name: { type: "string" },
      },
      relationships: {
        homeMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "homeTeam",
        },
        awayMatches: {
          type: "matches",
          cardinality: "many",
          inverse: "awayTeam",
        },
      },
    },
  },
};

Advanced Usage

Custom Validation

import {
  createValidator,
  validateSchema,
  ensureValidSchema,
} from "@spectragraph/core";

const customValidator = createValidator({
  ajvSchemas: [myCustomSchema],
});

// Option 1: Check errors manually
const errors = validateSchema(schema, { validator: customValidator });
if (errors.length > 0) {
  console.error("Schema validation failed:", errors);
}

// Option 2: Throw on validation failure
ensureValidSchema(schema, { validator: customValidator });

Performance Optimization

For better performance with large datasets:

  1. Link inverses once with linkInverses rather than on each query
  2. Validate schemas once during application startup

Related Packages

  • @spectragraph/memory-store - In-memory data store implementation
  • @spectragraph/postgres-store - PostgreSQL backend
  • @spectragraph/jsonapi-store - JSON:API client store