Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 75 additions & 66 deletions AGENTS.md

Large diffs are not rendered by default.

212 changes: 0 additions & 212 deletions docs/api-coverage.md

This file was deleted.

44 changes: 7 additions & 37 deletions src/assets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,6 @@ use std::sync::Weak;
/// Client for the `/assets` endpoints — the typed view of the `ASSET`-labelled corner of the
/// resource graph.
///
/// Every call here is the generic `/resources` pipeline with the `ASSET` discriminator pinned
/// server-side, so the two paths cannot drift apart on ACLs, naming policy or status codes. The
/// difference is the shape that comes back: [`Asset`] rather than the polymorphic
/// [`Node`](crate::nodes::Node), so `geolocation` and `is_root` are reachable without a match.
///
/// The filter and search bodies are the resource ones — [`ResourceFilterForm`] and
/// [`SearchAndFilterForm<ResourceFilter>`] — because the api declares the same Java types on both
/// endpoint families. A `node_type` set on either is **replaced**, not merged: the api pins
/// `["asset"]` over whatever the caller asked for, since `node_type` entries OR together and
/// leaving a caller's `["timeseries"]` in place would widen a request to `/assets` into a mixed
/// query.
///
/// `/assets` is the newest of the typed families. What it does *not* share with `/functions` is
/// worth knowing: functions have no `/byids`, `/filter` or `/search` on the api at all.
pub struct AssetsService {
pub(crate) api_service: Weak<ApiService>,
base_url: String,
Expand All @@ -50,16 +36,6 @@ impl AssetsService {

/// `POST /assets/create` — create one or more assets. Each needs a unique `external_id` and a
/// `name`; both are required server-side.
///
/// Unlike [`ResourceService::create`](crate::resources::ResourceService::create), the `ASSET`
/// type-label does not have to be set by the caller: the endpoint deserializes straight into
/// the api's `Asset`, whose constructor forces the label in. [`Asset::new`] leaves `labels`
/// unset for that reason, and any domain labels you do set are kept alongside it.
///
/// Relations are not creatable here — this takes nodes only. Use
/// [`ResourceService::create`](crate::resources::ResourceService::create) to build assets and
/// the edges between them in one call, or [`EdgesService`](crate::relations::EdgesService) to
/// link assets that already exist.
pub async fn create<I>(&self, data: &I) -> Result<DataWrapper<Asset>, ResponseError>
where
for<'a> &'a I: Into<DataWrapper<Asset>>,
Expand All @@ -70,12 +46,7 @@ impl AssetsService {
}

/// `GET /assets/{id}` — one asset by its numeric id.
///
/// **404 does not mean the id is free.** A node that exists but is not an asset, and an asset
/// the caller may not read, are both reported as missing — the api hides existence rather than
/// answering 403. So a 404 here says "not an asset you can read", nothing more.
///
/// Unlike [`by_ids`](Self::by_ids), which omits what it cannot find, this is an error.
/// Errors: 404 on a missing or inaccessible asset.
pub async fn get_by_id(&self, id: u64) -> Result<DataWrapper<Asset>, ResponseError> {
let path = &format!("{}/{}", self.base_url, id);
self.execute_get_request::<DataWrapper<Asset>, ()>(path, None)
Expand All @@ -96,22 +67,21 @@ impl AssetsService {
.await
}

/// `GET /assets?limit=N` — the first `limit` assets you may read, newest created first.
/// `GET /assets?limit=N`
///
/// `None` sends no `limit` and leaves the server's default of 1000 in place; the maximum is
/// 10000, above which the server answers 400 rather than clamping.
/// 10000, above which the server answers 400 rather than clamping. Newest created first.
///
/// Assets are the bulk of a tenant, so this is a sample rather than an inventory, and there is
/// no paging: the api nulls `next_cursor` here deliberately, because a walk needs a `sort` and
/// a `cursor` and both live in a request body. Narrow with [`filter`](Self::filter) rather
/// than raising the number.
/// There is no paging: the api nulls `next_cursor` here deliberately, because a walk needs a `sort` and
/// a `cursor` and both live in a request body. Narrow with [`filter`](Self::filter) rather
/// than raising the number.
pub async fn list(&self, limit: Option<u64>) -> Result<DataWrapper<Asset>, ResponseError> {
let query = limit.map(|limit| [("limit", limit)]);
self.execute_get_request::<DataWrapper<Asset>, _>(&self.base_url, query.as_ref())
.await
}

/// `POST /assets/filter` — the structured query, narrowed to assets.
/// `POST /assets/filter`
///
/// Takes the resource filter body unchanged, so [`ResourceFilter::is_root`] and
/// [`ResourceFilter::data_set_id`] are available here and the shared
Expand Down
8 changes: 4 additions & 4 deletions src/datasets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ impl DatasetsService {
/// `POST /datasets/filter` — datasets matching [`DatasetFilterForm`], newest first.
///
/// Every criterion on the filter is honoured server-side. Results are capped by the form's
/// `limit` (default 100, max 10000) and there is no paging, so a filter broad enough to exceed
/// the cap is silently truncated — narrow it rather than trying to page.
/// `limit` (default 100 on the form, 1000 server-side when unset; max 10000). A broader match
/// than the cap is paged, not truncated: set [`sort`](DatasetFilterForm) and echo the
/// envelope's `next_cursor` back as `cursor` to walk it, under that same `sort`.
pub async fn filter(
&self,
filter: &DatasetFilterForm,
Expand Down Expand Up @@ -102,8 +103,7 @@ impl DatasetsService {
/// scored before `limit` applies.
///
/// The whole form reaches the server: `filter` narrows the phrase's hits and can never widen
/// them. No match is an empty item list, not an error — the 404 the OpenAPI annotation still
/// advertises was removed server-side.
/// them. No match is an empty item list, not an error.
///
/// `query` is validated at 3–140 characters and nothing else. It used to be held to
/// `^[\p{IsLatin}\p{Zs}\p{Nd}]+` as well, which rejected every snake_case external id and
Expand Down
18 changes: 13 additions & 5 deletions src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,11 @@ impl EventsService {
.await
}

/// Look up a single event by its UUID `id` (`GET /events/{id}`). The result is an empty
/// `items` if no event with that id exists (or it belongs to a tenant you can't read).
/// Look up a single event by its UUID `id` (`GET /events/{id}`).
///
/// A miss is a **404** carrying a `not-found` problem, not an empty `items`. An event outside
/// the caller's readable datasets answers the same way, so a 404 says "not an event you can
/// read" and never leaks that the id exists.
pub async fn get(&self, id: &Uuid) -> Result<DataWrapper<Event>, ResponseError> {
let path = &format!("{}/{}", self.base_url, id);
self.execute_get_request(path, None::<&str>).await
Expand All @@ -227,9 +230,14 @@ impl EventsService {
.await
}

/// Free-text search over event descriptions (`POST /events/search`). Matching is fuzzy and
/// word-aware; results are ranked by relevance. For structured filters (time ranges, types,
/// related resources) use [`filter`](Self::filter) instead — it is faster and more predictable.
/// Free-text search over events (`POST /events/search`).
///
/// A plain case-insensitive **substring** match — not fuzzy, not word-aware — across
/// `externalId`, `description` and the metadata *values*. Events live in ClickHouse rather
/// than the node table and have no full-text index, so results are **not ranked**: they come
/// back newest first by `eventTime`, unlike the three node searches. For structured filters
/// (time ranges, types, related resources) use [`filter`](Self::filter) instead — it is faster
/// and more predictable.
pub async fn search(
&self,
search: &SearchAndFilterForm<EventFilter>,
Expand Down
Loading
Loading