xmemory-relations
v1.4.0
Published
Typed, equivalence-aware associations between Things for XMemory
Readme
xmemory-relations
Typed, equivalence-aware associations between Things for XMemory. Stores and queries directed and undirected edges with filters and pagination, using nx-mongo for persistence and xmemory-equal for thing identity and canonicalization.
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), ornone - ThingRef: supports
thingType(e.g.question,product_name) and optionalexternalIdentifier(e.g.jira:PROJ-123,github:issue:owner/repo#14) for “go back to source” pointers; passed through to xmemory-equal - Endpoints:
getAssociationsandgetEdgereturn fullThingRecords forfrom/to, includingthingTypeandexternalIdentifierwhen provided by xmemory-equal (e.g. when it exposesgetThingById) - Edge-centric retrieval:
getEdgeById(edgeId),getEdgesByIds(edgeIds),getAssociationsBetween(a, b, query?),findEdges(query)(by type, direction, endpoint IDs, confidence, source, metadata), andgetRelatedNodes(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,drivingFieldRelationIdon associations for mapper use; stored as top-level fields and filterable infindEdges - No Mongo client creation: you pass an initialized nx-mongo handle and an xmemory-equal client
Install
npm install xmemory-relations nx-mongo xmemory-equalRequirements
- nx-mongo: initialized (e.g.
SimpleMongoHelper) with your MongoDB connection; relations use it for the{prefix}_associationscollection. - xmemory-equal:
EqualClientcreated with the same namespace and a compatiblenxMongoprovider (e.g. same DB). Used forensureThing,resolve, andgetEquivalents. If it exposesgetThingById(id), relations use it to return full thing records (includingexternalIdentifier) for association endpoints; optionalgetThingByExternalIdentifier(externalId, namespace?)allows querying by external id ingetAssociations(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.
Usage
import { SimpleMongoHelper } from "nx-mongo";
import { createEqualClient } from "xmemory-equal";
import { createRelationsClient } from "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"] });API summary
createRelationsClient(config)→RelationsClient- Required:
nxMongo,nxEqual,namespace - Optional:
database,collectionPrefix,writeEqualityMode,readEqualityMode
- Required:
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. OptionalreadEqualityModeoverride. Ifthinghas onlyexternalIdentifier(and namespace), relations resolve it via equal’sgetThingByExternalIdentifierwhen available. Returns{ items, total, limit, offset }with full thing records for endpoints (includingthingTypeandexternalIdentifierwhen provided by equal).getEdgeById(edgeId)→Promise<AssociationRecord | null>
Returns the association document by its Mongo_id(hex string). Returnsnullfor 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). Optionalquery: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 asgetAssociations; 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):
ensureThingthenresolveto canonical IDs before storing → no duplicate edges across equivalent things. - none: store using the IDs returned by
ensureThing(no resolve).
- canonical (default):
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
ExpandLimitExceededErrorif exceeded). - none: query the single thing ID only.
Scope-aware retrieval and multi-DB
- Scope filter (
scope: { db?, collection? }): When provided ingetAssociations(thing, query)orfindEdges(query), only edges whose both endpoints (Things) match the givendband/orcollectionare returned. Scope lives on Things in@xmemory/equal; relations resolve endpoints viagetThingByIdand filter by optionaldb/collectionon the returnedThingRecord. If equal does not store or return these fields, scope filtering has no effect. - Endpoint thing types (
endpointThingTypes: string[]): Only edges whose both endpoints havethingTypein 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/equalat 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 exposesdb/collectionon ThingRecords and thatgetThingById(or optionallistThings) 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; usegetEdgeById(edgeId)with the hex string of_id(e.g. fromgetEdge(…).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
