Skip to content

Feat/polymorphic nodes - #108

Merged
olavgg merged 3 commits into
mainfrom
feat/polymorphic-nodes
Sep 2, 2026
Merged

Feat/polymorphic nodes#108
olavgg merged 3 commits into
mainfrom
feat/polymorphic-nodes

Conversation

@JosteinGj

Copy link
Copy Markdown
Contributor

No description provided.

jgjesdal and others added 3 commits August 26, 2026 10:50
/resources is the generic node query - it spans assets, timeseries,
functions, resources, data sets and policies - but every row arrived
coerced into a flat Resource. A timeseries read this way had no unit, a
data set no policies, and both carried an isRoot flag meaningless for
their type. The api's node-reads/* and node-create/* branches fixed the
server half; this is the SDK half.

`Node` (src/nodes.rs) is an enum over Asset, TimeSeries, Function,
Resource, Dataset and Policy. filter/search/get_by_id return
DataWrapper<Node>, by_ids/create and EdgesService::by_ids return
GraphDataWrapper<Node>, and ResourceNetwork::nodes is Vec<Node>.

The discriminator is a label, not a field: there is no nodeType key on
the wire, so `Node` hand-writes Deserialize - buffer, canonicalize each
label the way the api's TextValidator does, dispatch. No type-label means
RESOURCE; more than one is an error, as it is server-side. Serializing
emits the variant's own shape and appends its type-label if absent, and
never strips a conflicting one: a body labelled both ASSET and POLICY
earns the api's 400 naming both, which is the accurate answer.

Also here, because the typed shapes need them:

- TimeSeries gains labels and table_engine, Function gains description,
  source and data_set_id, Dataset gains labels, source, related_resources
  and data_set_id. Each is a field the api already sends and the SDK
  silently dropped.
- Dataset timestamps move from FixedOffset to Utc, matching every other
  DTO, and connectedDataSets now reads the JSON strings the api actually
  sends rather than failing on them - latent only because reads never
  populate it.
- Python maps each variant to its own pyclass, so isinstance works and an
  object from resources.filter() behaves like one from timeseries.by_ids().
  New Asset and Policy classes; node_type on every node class for
  data-driven dispatch. The dispatch is a hand-written IntoPyObject on a
  non-pyclass PyNode wrapper, the first such impl in the bindings.
- Fixes a panic in the Python bindings' resources.by_ids on an empty
  graph response.

update is deliberately untouched: the api still echoes every node as a
flat Resource there, so ResourceService::update keeps returning
GraphDataWrapper<Resource>. That asymmetry is the api's NODE_UPDATE_REFACTOR
to remove.

Gated on the api. The ten branches this consumes are not merged, so
against a backend built from master the typed reads fail on a missing
valueType - seven python tests, all one cause. Do not publish 0.3.0 until
they land.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running the suites against a backend carrying the api's node-reads and
node-create branches turned up three things the earlier gated run could
not reach:

- `Asset` and `Policy` had `neighbors` but not `related_events`, so a
  node created with the ASSET label silently lost a method it had as a
  `Resource` (test_navigation caught it as an AttributeError). Both are
  now generated by the same macro as `neighbors`, which is why they
  diverged in the first place: the four older node classes each spell
  these out by hand, and writing them a fifth and sixth time is what
  dropped one.

- A graph-sourced timeseries reports `securityCategories: []`, not an
  absent field - `Timeseries` has no NON_NULL include, so the api emits
  its DTO default. The assertion and both doc claims said "absent". The
  sharper statement, now recorded: on a graph read *nothing* is missing,
  so a caller cannot tell a default from a real value and must re-read
  the node flatly.

- test_polymorphic_nodes built a plain resource with no labels at all;
  the api requires at least one. A plain resource is the node carrying no
  *type* label, not one carrying none. Its graph-sparseness test also
  skipped when the projection lagged, which let the assertion quietly
  never run - it polls now.

Rust: 201 passed, 1 failed (mcp_event_update_by_uuid_reindexes_the_external_id,
red on purpose). Python: 582 passed, 7 failed, none of them new - four
multi-tenant and three errors are 403 "unknown organization" from a
deployment without those tenants, two are datapoint fixtures with no data,
and one is a pre-existing brittle unfiltered event query that only became
reachable now that its fixture builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running the suites against the upgraded api found a real defect: any
`fetch_related`/`fetch_nearest` traversal reaching a timeseries failed with
`missing field valueType`. A graph read carries the shared node fields and
nothing else -

  ['createdTime','dataSetId','externalId','id','labels','lastUpdatedTime',
   'metadata','name','relatedResources']

- so no unit, unitExternalId, valueType, tableEngine or securityCategories.
They are absent from the payload, not defaulted, which is what the earlier
doc claims got wrong.

That invalidates the premise this field was left required on. The reasoning
was "the api always sends valueType, so gate the SDK on the backend rather
than weaken the type". True of the flat reads, false of the graph ones - and
a required field there is not a stricter contract, it is a hard error on a
call that has nothing to do with value types. `Option<String>` also puts it
where its siblings already are: unit, table_engine and security_categories
are all optional and all None on this path.

Rejected `#[serde(default)]`: it would yield "" for a series whose real type
is BIGINT, so `if ts.value_type == "float"` answers wrong in silence. None
means "not told", which a caller can act on.

Corrects three places that repeated the wrong claim (the nodes module doc,
AGENTS.md, and the neo4j field test, which asserted securityCategories was an
empty list rather than absent), and pins the real shape in
`a_graph_sourced_timeseries_has_none_of_its_type_specific_fields`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JosteinGj
JosteinGj requested a review from olavgg September 2, 2026 10:38
@olavgg
olavgg merged commit 3962657 into main Sep 2, 2026
32 of 34 checks passed
@olavgg
olavgg deleted the feat/polymorphic-nodes branch September 2, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants