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

@x12i/xmemory-relations

v1.7.0

Published

Typed, equivalence-aware associations between Things for XMemory

Readme

@xronoces/xmemory-relations

Typed, equivalence-aware associations between Things for XMemory. Stores and queries directed and undirected edges with filters and pagination. Persistence via nx-mongo or @xronoces/xronox; thing identity via @xronoces/xmemory-equal.

Published: GitHub Packages (@xronoces scope). Configure .npmrc with @xronoces:registry=https://npm.pkg.github.com and auth token for npm.pkg.github.com.

Features

  • Upsert/delete directed and undirected associations (idempotent by unique identity)
  • Query with filters (type, direction, confidence, tags, source) and pagination
  • Canonical by default: write and read use canonical thing IDs so equivalent aliases do not create duplicate edges
  • Optional read modes: canonical, expand (all equivalents), or none
  • ThingRef: supports thingType (e.g. question, product_name) and optional externalIdentifier (e.g. jira:PROJ-123, github:issue:owner/repo#14) for “go back to source” pointers; passed through to xmemory-equal
  • Endpoints: getAssociations and getEdge return full ThingRecords for from/to, including thingType and externalIdentifier when provided by xmemory-equal (e.g. when it exposes getThingById)
  • Edge-centric retrieval: getEdgeById(edgeId), getEdgesByIds(edgeIds), getAssociationsBetween(a, b, query?), findEdges(query) (by type, direction, endpoint IDs, confidence, source, metadata), and getRelatedNodes(thing, query?) (deduped neighbor nodes + edges)
  • Scope-aware filtering: retrieval uses xEqual-assisted filtering (readEqualityMode: canonical / expand / none). Optional endpoint scope (scope: { db?, collection? }) and endpointThingTypes so Scoper can stay within scope; filter by sessionId / drivingFieldRelationId for mapper
  • Bulk upsert: bulkAssociate(items) for mapper and batch workloads; idempotent per edge
  • Mapper metadata: optional sessionId, drivingFieldRelationId on associations for mapper use; stored as top-level fields and filterable in findEdges
  • Data tier: pass an nx-mongo–backed provider (e.g. SimpleMongoHelper) or use createXronoxRelationsProvider(xronox, { role }) to run on @xronoces/xronox with the same role as the rest of the stack (e.g. mapping_metadata).

Install

npm install @xronoces/xmemory-relations @xronoces/xmemory-equal nx-mongo

For xronox as data tier (optional):

npm install @xronoces/xmemory-relations @xronoces/xmemory-equal @xronoces/xronox

Configure .npmrc for GitHub Packages:

@xronoces:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN

Requirements

  • Data tier: either
    • nx-mongo: initialized (e.g. SimpleMongoHelper) with your MongoDB connection; relations use it for the {prefix}_associations collection; or
    • xronox: use createXronoxRelationsProvider(xronox, { role: "mapping_metadata" }) and pass it as nxMongo. The role must match a binding in your xronox config. Limitation: removeAssociation (delete) is not supported when using the xronox provider (xronox does not expose delete); use nx-mongo for full CRUD if you need deletes.
  • @xronoces/xmemory-equal: EqualClient created with the same namespace and a compatible data tier. Used for ensureThing, resolve, and getEquivalents. If it exposes getThingById(id), relations use it to return full thing records (including externalIdentifier) for association endpoints; optional getThingByExternalIdentifier(externalId, namespace?) allows querying by external id in getAssociations(thing).

ThingRef and ThingRecord

Inputs use ThingRef (from xmemory-equal): namespace, thingType, kind, ref, and optional externalIdentifier. Thing identity is (namespace, thingType, kind, refNorm); externalIdentifier is a stable pointer to an external system (e.g. jira:PROJ-123, confluence:page:123, futurox:question:uuid). All association APIs accept ThingRef and pass it through to equal.

Returned ThingRecords (on AssociationRecord.from and .to) include id, namespace, thingType, kind, refRaw, refNorm, optional externalIdentifier, and timestamps. When xmemory-equal provides getThingById, relations use it so endpoints are full records from equal; otherwise a minimal record shape is returned.

Config and env

The client can read these from process.env (optional overrides in config):

| Env | Description | Default | |-----|-------------|--------| | XMEMORY_WRITE_EQUALITY_MODE | canonical or none | canonical | | XMEMORY_READ_EQUALITY_MODE | canonical, expand, or none | canonical | | XMEMORY_NAMESPACE | Resolved namespace (shared with equal) | — | | XMEMORY_COLLECTION_PREFIX | Collection name = {prefix}_associations | xmemory |

Important: This package does not read MONGO_URI or MONGO_DB. You configure and initialize nx-mongo yourself and pass it in.

Database for associations (required)

Associations are written to the {prefix}_associations collection in a single MongoDB database. You must specify which database in one of two ways; otherwise the client throws at creation time to avoid writing to the wrong DB (e.g. the connection default admin):

  1. Explicit database in config: pass database: "xmemory-meta" (or your DB name). Use this when your nxMongo helper does not expose the target DB name.

  2. Provider with getDatabaseName(): implement optional getDatabaseName?: () => string on the object you pass as nxMongo. The client will call it when database is not set and use the returned name for all reads/writes. Example wrapper when your helper has getDb():

    nxMongo: {
      ...metaHelper,
      getDatabaseName: () => metaHelper.getDb().databaseName,
    }

If neither database nor nxMongo.getDatabaseName() is provided (or getDatabaseName() returns undefined/empty), createRelationsClient throws: "RelationsConfig: database must be set or nxMongo must provide getDatabaseName() so associations are not written to the wrong database."

Usage

With nx-mongo

import { SimpleMongoHelper } from "nx-mongo";
import { createEqualClient } from "@xronoces/xmemory-equal";
import { createRelationsClient } from "@xronoces/xmemory-relations";

const helper = new SimpleMongoHelper(process.env.MONGO_URI!);
await helper.initialize({ databaseName: process.env.MONGO_XMEMORY_DB });

const nxEqual = createEqualClient({
  nxMongo: { getDb: () => helper.getDatabaseByName(process.env.MONGO_XMEMORY_DB!), withTransaction: (cb) => helper.withTransaction(cb) },
  namespace: "my-namespace",
  defaultThingType: "generic",
});
await nxEqual.ensureIndexes();

const relations = createRelationsClient({
  nxMongo: helper,
  nxEqual,
  namespace: "my-namespace",
  database: process.env.MONGO_XMEMORY_DB,
});
await relations.ensureIndexes();

// Directed association (thingType and optional externalIdentifier)
await relations.associate(
  { namespace: "my-namespace", thingType: "person_name", kind: "literal", ref: "user-1" },
  { namespace: "my-namespace", thingType: "person_name", kind: "literal", ref: "user-2" },
  "follows",
  { direction: "directed", confidence: 0.9 }
);

// Undirected
await relations.associate(thingRef("a"), thingRef("b"), "related", { direction: "undirected" });

// Query
const result = await relations.getAssociations(thingRef("user-1"), {
  types: ["follows"],
  direction: "outgoing",
  limit: 50,
  offset: 0,
});

// Remove
const { deleted } = await relations.removeAssociation(thingRef("a"), thingRef("b"), "related", { direction: "undirected" });

// Single edge (record.id is the Mongo _id when loaded from store)
const edge = await relations.getEdge(thingRef("a"), thingRef("b"), "related", { direction: "undirected" });

// Edge-centric: by id, between two nodes, or find by type/metadata
const one = await relations.getEdgeById(edge!.id);
const between = await relations.getAssociationsBetween(thingRef("a"), thingRef("b"), { types: ["related"] });
const { edges } = await relations.findEdges({ types: ["follows"], limit: 20 });
const { nodes, edges: nodeEdges } = await relations.getRelatedNodes(thingRef("user-1"), { types: ["follows"] });

With xronox (same role as mapper / equal)

When the rest of the stack uses @xronoces/xronox, use createXronoxRelationsProvider so relations use the same role and config:

import { createXronox } from "@xronoces/xronox";
import { createEqualClient } from "@xronoces/xmemory-equal";
import { createRelationsClient, createXronoxRelationsProvider } from "@xronoces/xmemory-relations";

await xronox.init({ engine: "nxMongo", engineConfig: { nxMongo: { connectionString: process.env.MONGO_URI } } });

const nxEqual = createEqualClient({ nxMongo: /* your equal provider */, namespace: "my-namespace" });
const nxMongo = createXronoxRelationsProvider(xronox, { role: "mapping_metadata" });

const relations = createRelationsClient({
  nxMongo,
  nxEqual,
  namespace: "my-namespace",
});
await relations.ensureIndexes();
// associate, getAssociations, findEdges, bulkAssociate work as usual.
// removeAssociation is not supported with xronox provider (throws); use nx-mongo if you need deletes.

API summary

  • createRelationsClient(config)RelationsClient

    • Required: nxMongo, nxEqual, namespace
    • Database: either pass database (string) or use an nxMongo provider that implements getDatabaseName(). If neither is set, creation throws so associations are not written to the wrong DB.
    • Optional: collectionPrefix, writeEqualityMode, readEqualityMode
  • associate(from, to, type, opts?)Promise<AssociationRecord>
    Upserts one association (directed or undirected). Optional: direction, confidence, weight, source, tags, reason, sessionId, drivingFieldRelationId.

  • bulkAssociate(items: BulkAssociateItem[])Promise<AssociationRecord[]>
    Bulk upsert (required for mapper). Each item: { from, to, type, opts? }. Idempotent per edge.

  • removeAssociation(from, to, type, opts?)Promise<{ deleted: number }>
    Deletes by identity (0 or 1).

  • getAssociations(thing, query?)Promise<AssociationQueryResult>
    Filters: types, direction (incoming/outgoing/both), confidenceMin/confidenceMax, tagsAny, source. Scope-aware: scope?: { db?, collection? }, endpointThingTypes?: string[] (only edges whose both endpoints match). Pagination: limit (default 50, max 500), offset. Optional readEqualityMode override. If thing has only externalIdentifier (and namespace), relations resolve it via equal’s getThingByExternalIdentifier when available. Returns { items, total, limit, offset } with full thing records for endpoints (including thingType and externalIdentifier when provided by equal).

  • getEdgeById(edgeId)Promise<AssociationRecord | null>
    Returns the association document by its Mongo _id (hex string). Returns null for invalid or missing id.

  • getEdgesByIds(edgeIds)Promise<AssociationRecord[]>
    Returns association records for the given edge ids (invalid ids are skipped).

  • getAssociationsBetween(a, b, query?)Promise<AssociationRecord[]>
    Returns all edges between two things (directed both ways + undirected). Optional query: types, limit, offset.

  • findEdges(query)Promise<FindEdgesResult>
    Edge-centric query: types, direction (directed/undirected/any), endpointThingIds, confidenceMin/confidenceMax, source, sessionId, drivingFieldRelationId, metadataEquals (key-value match on top-level fields). Scope-aware: scope?: { db?, collection? }, endpointThingTypes?: string[]. limit, offset. Returns { edges, total, limit, offset }. Required for mapper/scoper reporting.

  • getRelatedNodes(thing, query?)Promise<GetRelatedNodesResult>
    Same filters as getAssociations; returns { nodes, edges, total, limit, offset } with deduped neighbor nodes (useful for UI and mapper reports).

  • ensureIndexes()Promise<void>
    Ensures unique and query indexes on the associations collection (with partial filters for directed vs undirected).

  • getEdge(from, to, type, opts?)Promise<AssociationRecord | null>

Equality modes

  • Write

    • canonical (default): ensureThing then resolve to canonical IDs before storing → no duplicate edges across equivalent things.
    • none: store using the IDs returned by ensureThing (no resolve).
  • Read

    • canonical (default): resolve the input thing to one canonical ID and query that.
    • expand: resolve to all equivalent IDs, query all, dedupe (max group size 500; throws ExpandLimitExceededError if exceeded).
    • none: query the single thing ID only.

Scope-aware retrieval and multi-DB

  • Scope filter (scope: { db?, collection? }): When provided in getAssociations(thing, query) or findEdges(query), only edges whose both endpoints (Things) match the given db and/or collection are returned. Scope lives on Things in @xmemory/equal; relations resolve endpoints via getThingById and filter by optional db/collection on the returned ThingRecord. If equal does not store or return these fields, scope filtering has no effect.
  • Endpoint thing types (endpointThingTypes: string[]): Only edges whose both endpoints have thingType in this list are returned.
  • Multi-DB limitation: Relations store only Thing IDs; they do not store database or collection names. Scope (db/collection) is resolved from @xmemory/equal at read time. Cross-DB or multi-dB edge traversal is therefore limited to what equal can resolve per namespace; when using multiple MongoDB databases or collections for Things, ensure equal exposes db/collection on ThingRecords and that getThingById (or optional listThings) is consistent with that model.

Collection and indexes

  • Collection: {collectionPrefix}_associations (e.g. xmemory_associations).
  • Directed docs: namespace, direction: "directed", type, fromThingId, toThingId, plus optional metadata (confidence, weight, source, tags, reason, sessionId, drivingFieldRelationId) and timestamps.
  • Undirected docs: same but leftThingId / rightThingId (min/max of the two thing IDs).
  • Unique indexes (with partial filter by direction) and query indexes for outgoing/incoming/undirected lookups.
  • Each document has a Mongo _id; use getEdgeById(edgeId) with the hex string of _id (e.g. from getEdge(…).id).

See docs/specs.md for the full specification.

Errors

  • NamespaceMismatchError: config namespace does not match the one expected for the Equal client.
  • ExpandLimitExceededError: expand-mode read would exceed the max equivalence group size (500).

License

ISC