Skip to content

Things

A thing is a row in things with one thing_references row per place its bytes live. The bytes themselves stay in the resource they came from.

app/models/thing.rb · app/models/thing_reference.rb

kind what it is — pdf, email, image… Required
title display name, usually the basename of the locator key
parent_id set on a thing extracted from another, such as an email attachment

Thing#analyzed_at is the maximum analyzed_at across its references, not a column.

resource_id which resource holds this copy
locator_key the key within it — unique per (tenant_id, resource_id)
locator the type-specific address, as JSON
version what the resource last reported, usually an ETag
changed_at set when version changes, which also clears analyzed_at
analysis the step machine’s record — see Analysis

ThingReference.discover! is the sync path: it finds or creates the reference, creates a thing if there is not one yet, and calls note_version!. A changed version re-opens analysis.

ThingReference.record! is the export path: it attaches a reference to a thing you already have, and records source_version so stale_against? can tell whether the copy has fallen behind.

Assigned from the file extension by Kind.for_filename (app/models/kind.rb), defaulting to file.

kind extensions
pdf pdf
image png, jpg, jpeg, gif, webp, heic, tif, tiff
text txt, md, rtf
data csv, tsv, json, xml
xlsx xlsx, xls, ods
doc doc, docx, odt
calendar ics
contact vcf, vcard
pkpass pkpass
email eml
file anything else

Kind is not resource type. pdf is a kind; s3 is a type.

An analyzer that declares has_children? returns child bodies from children_of, and Analyzer::Base writes them into a Resource::Database resource keyed children before analyzing the parent. Thing::DEPTH caps the chain at 4.

Thing#body_text is the union of the strings extracted from a thing’s own references and its children’s, so an email matches on a word that only appears in an attached PDF.

Thing#children_ready? is true when every child has an analyzed_at. A parent that found children returns without analyzing; the last child to finish re-enqueues it (AnalyzeThingJob#wake_parent).

Thing#merge! moves every reference off another thing onto this one. Blocking groups candidates, ProposeMergesJob writes MergeProposal rows, and the GraphQL mutations mergeThings, settleMergeProposal and splitReference act on them.

Deleting a thing and deleting the bytes it points at are separate operations.

Does Reversible by
destroy the thing drops the row, its references and its analysis re-syncing the resource
delete the object removes the bytes from the resource nothing

Thing dependent: :destroy cascades to references and children. Nothing in the catalog deletes a resource’s objects.

One table, cursor pagination through Page.of, OpenSearch as the query path. count(*) is avoided on catalog-sized scopes.