xmemory-equal
v1.4.0
Published
Canonical Thing identity and equality layer for XMemory (MongoDB via nx-mongo)
Downloads
423
Readme
xmemory-equal
Canonical Thing identity and 100% equality layer for XMemory. Stores things (literal text or keys), links them as equivalent, and resolves to a canonical representative. Persists to MongoDB via nx-mongo only (this package does not create Mongo clients or read MONGO_URI).
Install
npm install xmemory-equal nx-mongoUsage
- Create and initialize an nx-mongo helper (your app owns the connection and DB name):
import { SimpleMongoHelper } from "nx-mongo";
const helper = new SimpleMongoHelper(process.env.MONGO_URI!);
await helper.initialize({ databaseName: process.env.MONGO_XMEMORY_DB ?? "xmemory" });- Create the equal client and ensure indexes:
import { createEqualClient } from "xmemory-equal";
const equal = createEqualClient({
nxMongo: helper,
namespace: "myapp", // optional; default from XMEMORY_NAMESPACE or "default"
defaultThingType: "generic", // optional; default from XMEMORY_DEFAULT_THING_TYPE or "generic"
normalization: "basic", // optional; default "basic"
canonicalStrategy: "preferKey" // optional; default "preferKey"
});
await equal.ensureIndexes();- Ensure things, link them, and query:
const ns = "myapp";
// Thing identity = (namespace, thingType, kind, refNorm). Same string can be different things by type.
const aliceKey = await equal.ensureThing({ namespace: ns, thingType: "person_name", kind: "key", ref: "user:alice" });
const aliceLit = await equal.ensureThing({ namespace: ns, thingType: "person_name", kind: "literal", ref: "Alice" });
// Link = same real-world thing (equivalence; transitive)
await equal.link(aliceKey, aliceLit, { reason: "same person" });
// Query
const equivs = await equal.getEquivalents(aliceKey); // all things in the same group
const same = await equal.areEqual(aliceKey, aliceLit); // true
const canon = await equal.resolve(aliceKey); // canonical representative (by strategy)ThingType: Use thingType to distinguish what the “something” is (e.g. person_name, company_name, product_name, doc_title, question, prompt, generic). The same ref in two different types are different things unless you explicitly link them. You can omit thingType and it will be filled from defaultThingType config or XMEMORY_DEFAULT_THING_TYPE (default generic).
externalIdentifier: Optional string on ThingRef to store a pointer to an external system (e.g. CRM id, external DB key). Stored in xmemory_things and returned on ThingRecord. If you set one that another thing in the same namespace already has, the client throws ExternalIdentifierConflictError.
metadata / data: Things can carry optional metadata (properties about the thing) and data (payload/snapshot). Pass them on ensureThing(thing, { metadata, data }) or patch later with updateThing(thingOrId, { set: { metadata, data } }) or merge: { metadata, data } for deep-merge. Each update increments version. Reserved metadata keys metadata.db and metadata.collection are the platform-standard scope (see Scope below). Use opts.scope on ensure or setThingScope(thingOrId, { db, collection }) to set them.
Soft delete: deleteThing(thingOrId, { deletedBy?, deleteReason? }) sets deletedAt (and optional deletedBy/deleteReason). By default, all read methods exclude deleted things; use { includeDeleted: true } to include them. purgeThing(thingOrId) hard-deletes the thing and its group membership (and optionally audit links).
API (summary)
| Method | Description |
|--------|-------------|
| ensureThing(thing, opts?) | Upsert thing; optional opts.metadata, opts.data, opts.scope (writes metadata.db, metadata.collection). Returns record with version, metadata, data. |
| updateThing(thingOrId, patch) | Patch with set (replace), merge (deep-merge), unset, unsetPaths. Reserved keys metadata.db/metadata.collection can be set/merged via metadata. Increments version. |
| setThingScope(thingOrId, { db, collection }) | Set reserved scope on a thing (merge into metadata). Returns updated ThingRecord. |
| deleteThing(thingOrId, opts?) | Soft delete (sets deletedAt). Returns { deleted: boolean }. |
| purgeThing(thingOrId) | Hard delete thing and group membership. |
| getThing(thing, opts?) | Read-only by identity. Returns null if not found or deleted (unless includeDeleted). |
| getThingById(id, opts?) | Read by id. |
| getThingByExternalIdentifier(ns, extId, opts?) | Read by namespace + externalIdentifier. |
| listThings(query) | List with thingTypes, kinds, db, collection (scope filters), externalIdentifierPrefix, updatedAfter/updatedBefore, includeDeleted (default false), limit/offset. |
| link(a, b, meta?) | Ensure both things, create/merge equivalence group; idempotent. |
| getEquivalents(thing, opts?) | All things in the same group (default excludes deleted). |
| areEqual(a, b) | true if same thing id or same group. |
| resolve(thing, opts?) | Canonical representative (default excludes deleted; recomputes if canonical is deleted). |
| setCanonical(thing) | When canonicalStrategy === "manualPinned", set the group’s canonical. |
| getEqualityLinks(thingOrId) | Audit edges where this thing is an endpoint. |
| getGroupLinks(groupId) | Audit edges within the group. |
| ensureIndexes() | Create required indexes on xmemory_* collections. |
| normalize(value) | Apply configured normalization. |
Config and env
Options can be set in code or via environment (env is used as default when the option is omitted):
| Option | Env | Default | Description |
|--------|-----|---------|-------------|
| namespace | XMEMORY_NAMESPACE | "default" | Logical partition / tenant. |
| defaultThingType | XMEMORY_DEFAULT_THING_TYPE | "generic" | Default thingType when omitted on ThingRef (e.g. person_name, doc_title, generic). |
| normalization | XMEMORY_NORMALIZATION | "basic" | none | basic | lowercase | unicode_nfkc. |
| canonicalStrategy | XMEMORY_CANONICAL_STRATEGY | "preferKey" | disabled | preferKey | preferLiteral | firstSeen | lexicographic | manualPinned. |
| collectionPrefix | XMEMORY_COLLECTION_PREFIX | "xmemory" | Collection names are {prefix}_things, {prefix}_groups, etc. |
| auditLinks | — | true | Write audit rows to xmemory_links on link. |
Important: This package does not read MONGO_URI or any DB name. Your app must create and initialize nx-mongo and pass it in as nxMongo.
Scope (metadata.db, metadata.collection)
Things can be scoped to a logical db and collection via reserved metadata keys metadata.db and metadata.collection. Scoper and Mapper rely on this; we do not scope using data.
- Set on create:
ensureThing(thing, { scope: { db: "mydb", collection: "items" } }) - Set or change later:
setThingScope(thingOrId, { db, collection })orupdateThing(thingOrId, { merge: { metadata: { db, collection } } }) - Retrieve by scope:
listThings({ thingTypes: ["doc_title"], db: "mydb", collection: "items" })
Multi-DB limitation: This package writes to a single MongoDB database (the one configured in your nx-mongo helper). The metadata.db / metadata.collection values are stored as strings and used only for filtering and application-level grouping. They do not change which MongoDB database or collection is used for persistence. To use multiple MongoDB databases you must create separate nx-mongo helpers (and equal clients) per database.
Identity rule
Thing identity is (namespace, thingType, kind, refNorm). So the same string can exist as different things:
"Apple"asthingType: "product_name"and"Apple"asthingType: "company_name"are two distinct things unless you explicitlylink()them.
Collections (in the DB selected by nx-mongo)
xmemory_things— thing rows: namespace, thingType, kind, refRaw, refNorm, optional externalIdentifier, metadata (optional reservedmetadata.db,metadata.collection), data, deletedAt, deletedBy, deleteReason, version. Unique on(namespace, thingType, kind, refNorm); sparse index on(namespace, externalIdentifier); indexes on(namespace, thingType, updatedAt)and(namespace, thingType, metadata.db, metadata.collection, updatedAt).xmemory_groups— equivalence groups (optional canonical, size, mergedIntoGroupId).xmemory_thing_group— thing ↔ group membership.xmemory_links— audit edges for links (optional).
Testing
Real DB tests require a running MongoDB and a .env with MONGO_URI and MONGO_XMEMORY_DB:
npm testOr run the TypeScript test directly:
npm run test:tsLicense
ISC
