diff --git a/docs/api/_index.md b/docs/api/_index.md index 7a90248..bc9a9b7 100644 --- a/docs/api/_index.md +++ b/docs/api/_index.md @@ -3,7 +3,7 @@ | Module | Endpoints | | --- | ---: | | [`ai`](./ai.md) | 16 | -| [`database`](./database.md) | 18 | +| [`database`](./database.md) | 22 | | [`echo`](./echo.md) | 1 | | [`files`](./files.md) | 10 | | [`membership`](./membership.md) | 18 | diff --git a/docs/api/database.md b/docs/api/database.md index 7bbae72..9a0bd5b 100644 --- a/docs/api/database.md +++ b/docs/api/database.md @@ -24,6 +24,8 @@ Access with `norbix.api.database`. | `replace_one` | `PUT` | `/{version}/database/collections/{collectionName}/{id}/replace` | `project` | | `update_many` | `PUT` | `/{version}/database/collections/{collectionName}/many` | `project` | | `update_one` | `PUT` | `/{version}/database/collections/{collectionName}/{id}` | `project` | +| `find_merged_term_tree` | `GET` | `/{version}/database/taxonomies/{taxonomyName}/merged-tree` | `project` | +| `find_own` | `GET` | `/{version}/database/collections/{collectionName}/own` | `project` | ## Working with terms @@ -315,3 +317,23 @@ structure = norbix.api.database.find_taxonomy_tree(include_terms=True) Now each taxonomy node's `terms` holds that taxonomy's full term tree (same shape as `find_term_tree`) — *Countries* carries its countries, *Cities* carries its cities. > Every term-reading call also accepts an optional `database_integration_id` to target a non-default database. + +--- + +### Records the caller owns + +**Goal:** list only the records of `products` that the signed-in user is +responsible for. + +```python +mine = norbix.api.database.find_own("products", pageSize=20) +``` + +### A taxonomy's merged term tree + +**Goal:** get one tree across taxonomies: the terms of `services` are the +roots, and the terms of its child taxonomies nest under them. + +```python +tree = norbix.api.database.find_merged_term_tree("services") +``` diff --git a/docs/hub/_index.md b/docs/hub/_index.md index 54b054b..d92e757 100644 --- a/docs/hub/_index.md +++ b/docs/hub/_index.md @@ -4,7 +4,7 @@ | --- | ---: | | [`account`](./account.md) | 61 | | [`ai`](./ai.md) | 14 | -| [`database`](./database.md) | 41 | +| [`database`](./database.md) | 67 | | [`echo`](./echo.md) | 1 | | [`email`](./email.md) | 1 | | [`files`](./files.md) | 22 | diff --git a/docs/hub/database.md b/docs/hub/database.md index 87c2578..3d8bb95 100644 --- a/docs/hub/database.md +++ b/docs/hub/database.md @@ -45,3 +45,62 @@ Access with `norbix.hub.database`. | `get_database_aggregates` | `GET` | `/{version}/database/aggregates` | `project` | | `save_database_aggregate` | `POST` | `/{version}/database/aggregates` | `project` | | `test_database_aggregate` | `POST` | `/{version}/database/aggregates/test` | `project` | +| `get_allowed_flex_tiers` | `GET` | `/{version}/database/integrations/flex-tiers` | `project` | +| `test_database_integration` | `POST` | `/{version}/database/integrations/test` | `project` | +| `reveal_managed_flex_connection_string` | `GET` | `/{version}/database/integrations/{Id}/connection-string` | `project` | +| `get_database_taxonomy_tree` | `GET` | `/{version}/database/taxonomies/tree` | `project` | +| `get_database_merged_term_tree` | `GET` | `/{version}/database/taxonomies/{TaxonomyName}/merged-tree` | `project` | +| `get_database_taxonomy_term_tree` | `GET` | `/{version}/database/taxonomies/{TaxonomyName}/terms/tree` | `project` | +| `apply_database_schema_bundle` | `POST` | `/{version}/database/schemas/apply-bundle` | `project` | +| `get_database_schema_list_settings` | `GET` | `/{version}/database/schemas/{Id}/list-settings` | `project` | +| `update_database_schema_list_settings` | `PUT` | `/{version}/database/schemas/{Id}/list-settings` | `project` | +| `update_database_schema_embed` | `PUT` | `/{version}/database/schemas/{Id}/embed` | `project` | +| `aggregate_records` | `POST` | `/{version}/database/collections/{collectionName}/aggregate` | `project` | +| `change_record_responsibility` | `PUT` | `/{version}/database/collections/{collectionName}/{id}/responsibility` | `project` | +| `count_records` | `GET` | `/{version}/database/collections/{collectionName}/count` | `project` | +| `delete_many_records` | `DELETE` | `/{version}/database/collections/{collectionName}/many` | `project` | +| `delete_record` | `DELETE` | `/{version}/database/collections/{collectionName}/{id}` | `project` | +| `distinct_record_values` | `GET` | `/{version}/database/collections/{collectionName}/distinct` | `project` | +| `execute_records_aggregate` | `POST` | `/{version}/database/collections/{collectionName}/aggregates/{aggregateId}/execute` | `project` | +| `find_records` | `GET` | `/{version}/database/collections/{collectionName}` | `project` | +| `find_one_record` | `GET` | `/{version}/database/collections/{collectionName}/{id}` | `project` | +| `get_collection_indexes` | `GET` | `/{version}/database/collections/{collectionName}/indexes` | `project` | +| `insert_many_records` | `POST` | `/{version}/database/collections/{collectionName}/many` | `project` | +| `insert_record` | `POST` | `/{version}/database/collections/{collectionName}` | `project` | +| `replace_record` | `PUT` | `/{version}/database/collections/{collectionName}/{id}/replace` | `project` | +| `seed_collection_records` | `POST` | `/{version}/database/collections/seed` | `project` | +| `update_many_records` | `PUT` | `/{version}/database/collections/{collectionName}/many` | `project` | +| `update_one_record` | `PUT` | `/{version}/database/collections/{collectionName}/{id}` | `project` | + +## Working with records (Hub) + +The Hub record methods are the admin side of collections: they act on every +record in the project, not only the records the caller owns (for that, use +`norbix.api.database.find_own`). Values go in as keyword arguments, the same +way as every other method in this SDK: on a `GET` they become query values, on +a write they become the JSON body. Filters, documents and updates are MongoDB +extended-JSON **strings**. + +```python +products = norbix.hub.database.find_records("products", filter='{"status": "active"}', pageSize=20) +one = norbix.hub.database.find_one_record("products", "rec_1") +created = norbix.hub.database.insert_record("products", document='{"title": "Lamp", "price": 12}') +norbix.hub.database.update_one_record("products", "rec_1", update='{"$set": {"price": 10}}') +norbix.hub.database.delete_record("products", "rec_1") +total = norbix.hub.database.count_records("products", filter='{"status": "active"}') +indexes = norbix.hub.database.get_collection_indexes("products") +``` + +## Trees, list settings, embed and bundles + +- `get_database_taxonomy_tree()` — the taxonomy structure as a tree. +- `get_database_taxonomy_term_tree("services")` — one taxonomy's terms as a tree. +- `get_database_merged_term_tree("services")` — one tree across taxonomies: the + terms of `services` are the roots, and the terms of its child taxonomies nest + under them. +- `get_database_schema_list_settings(id)` / `update_database_schema_list_settings(id, settings={...})` + — how the record list shows a schema (the update replaces the whole settings object). +- `update_database_schema_embed(id, ...)` — the schema's embed settings. +- `apply_database_schema_bundle(...)` — apply a bundle of schemas in one call. + +The async client (`AsyncNorbix`) has the same methods; `await` them. diff --git a/src/norbix_python/api/database.py b/src/norbix_python/api/database.py index 574ce7e..ab1685f 100644 --- a/src/norbix_python/api/database.py +++ b/src/norbix_python/api/database.py @@ -269,6 +269,32 @@ def update_one(self, collection_name: str, id: str, *, timeout: float | None = N bearer_token=bearer_token, ) + def find_merged_term_tree(self, taxonomy_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/{taxonomyName}/merged-tree""" + return self._transport.send( + target="api", + path="/{version}/database/taxonomies/{taxonomyName}/merged-tree", + method="GET", + path_params={"taxonomyName": taxonomy_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def find_own(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/own""" + return self._transport.send( + target="api", + path="/{version}/database/collections/{collectionName}/own", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + class AsyncDatabaseModule: def __init__(self, transport: AsyncTransport) -> None: @@ -533,3 +559,29 @@ async def update_one(self, collection_name: str, id: str, *, timeout: float | No timeout=timeout, bearer_token=bearer_token, ) + + async def find_merged_term_tree(self, taxonomy_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/{taxonomyName}/merged-tree""" + return await self._transport.send( + target="api", + path="/{version}/database/taxonomies/{taxonomyName}/merged-tree", + method="GET", + path_params={"taxonomyName": taxonomy_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def find_own(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/own""" + return await self._transport.send( + target="api", + path="/{version}/database/collections/{collectionName}/own", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) diff --git a/src/norbix_python/hub/database.py b/src/norbix_python/hub/database.py index e51ca04..4317565 100644 --- a/src/norbix_python/hub/database.py +++ b/src/norbix_python/hub/database.py @@ -581,6 +581,305 @@ def reveal_managed_flex_connection_string(self, id: str, *, timeout: float | Non bearer_token=bearer_token, ) + def get_database_taxonomy_tree(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/tree""" + return self._transport.send( + target="hub", + path="/{version}/database/taxonomies/tree", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_database_merged_term_tree(self, taxonomy_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/{TaxonomyName}/merged-tree""" + return self._transport.send( + target="hub", + path="/{version}/database/taxonomies/{TaxonomyName}/merged-tree", + method="GET", + path_params={"TaxonomyName": taxonomy_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_database_taxonomy_term_tree(self, taxonomy_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/{TaxonomyName}/terms/tree""" + return self._transport.send( + target="hub", + path="/{version}/database/taxonomies/{TaxonomyName}/terms/tree", + method="GET", + path_params={"TaxonomyName": taxonomy_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def apply_database_schema_bundle(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/schemas/apply-bundle""" + return self._transport.send( + target="hub", + path="/{version}/database/schemas/apply-bundle", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_database_schema_list_settings(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/schemas/{Id}/list-settings""" + return self._transport.send( + target="hub", + path="/{version}/database/schemas/{Id}/list-settings", + method="GET", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def update_database_schema_list_settings(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/schemas/{Id}/list-settings""" + return self._transport.send( + target="hub", + path="/{version}/database/schemas/{Id}/list-settings", + method="PUT", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def update_database_schema_embed(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/schemas/{Id}/embed""" + return self._transport.send( + target="hub", + path="/{version}/database/schemas/{Id}/embed", + method="PUT", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def aggregate_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}/aggregate""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/aggregate", + method="POST", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def change_record_responsibility(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/{id}/responsibility""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}/responsibility", + method="PUT", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def count_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/count""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/count", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def delete_many_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """DELETE /{version}/database/collections/{collectionName}/many""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/many", + method="DELETE", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def delete_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """DELETE /{version}/database/collections/{collectionName}/{id}""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}", + method="DELETE", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def distinct_record_values(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/distinct""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/distinct", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def execute_records_aggregate(self, collection_name: str, aggregate_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}/aggregates/{aggregateId}/execute""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/aggregates/{aggregateId}/execute", + method="POST", + path_params={"collectionName": collection_name, "aggregateId": aggregate_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def find_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def find_one_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/{id}""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}", + method="GET", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_collection_indexes(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/indexes""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/indexes", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def insert_many_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}/many""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/many", + method="POST", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def insert_record(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}", + method="POST", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def replace_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/{id}/replace""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}/replace", + method="PUT", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def seed_collection_records(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/seed""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/seed", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def update_many_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/many""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/many", + method="PUT", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def update_one_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/{id}""" + return self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}", + method="PUT", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + class AsyncDatabaseModule: def __init__(self, transport: AsyncTransport) -> None: @@ -1157,3 +1456,302 @@ async def reveal_managed_flex_connection_string(self, id: str, *, timeout: float timeout=timeout, bearer_token=bearer_token, ) + + async def get_database_taxonomy_tree(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/tree""" + return await self._transport.send( + target="hub", + path="/{version}/database/taxonomies/tree", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_database_merged_term_tree(self, taxonomy_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/{TaxonomyName}/merged-tree""" + return await self._transport.send( + target="hub", + path="/{version}/database/taxonomies/{TaxonomyName}/merged-tree", + method="GET", + path_params={"TaxonomyName": taxonomy_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_database_taxonomy_term_tree(self, taxonomy_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/taxonomies/{TaxonomyName}/terms/tree""" + return await self._transport.send( + target="hub", + path="/{version}/database/taxonomies/{TaxonomyName}/terms/tree", + method="GET", + path_params={"TaxonomyName": taxonomy_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def apply_database_schema_bundle(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/schemas/apply-bundle""" + return await self._transport.send( + target="hub", + path="/{version}/database/schemas/apply-bundle", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_database_schema_list_settings(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/schemas/{Id}/list-settings""" + return await self._transport.send( + target="hub", + path="/{version}/database/schemas/{Id}/list-settings", + method="GET", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def update_database_schema_list_settings(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/schemas/{Id}/list-settings""" + return await self._transport.send( + target="hub", + path="/{version}/database/schemas/{Id}/list-settings", + method="PUT", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def update_database_schema_embed(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/schemas/{Id}/embed""" + return await self._transport.send( + target="hub", + path="/{version}/database/schemas/{Id}/embed", + method="PUT", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def aggregate_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}/aggregate""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/aggregate", + method="POST", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def change_record_responsibility(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/{id}/responsibility""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}/responsibility", + method="PUT", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def count_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/count""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/count", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def delete_many_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """DELETE /{version}/database/collections/{collectionName}/many""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/many", + method="DELETE", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def delete_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """DELETE /{version}/database/collections/{collectionName}/{id}""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}", + method="DELETE", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def distinct_record_values(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/distinct""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/distinct", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def execute_records_aggregate(self, collection_name: str, aggregate_id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}/aggregates/{aggregateId}/execute""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/aggregates/{aggregateId}/execute", + method="POST", + path_params={"collectionName": collection_name, "aggregateId": aggregate_id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def find_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def find_one_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/{id}""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}", + method="GET", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_collection_indexes(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/database/collections/{collectionName}/indexes""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/indexes", + method="GET", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def insert_many_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}/many""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/many", + method="POST", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def insert_record(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/{collectionName}""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}", + method="POST", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def replace_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/{id}/replace""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}/replace", + method="PUT", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def seed_collection_records(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/database/collections/seed""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/seed", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def update_many_records(self, collection_name: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/many""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/many", + method="PUT", + path_params={"collectionName": collection_name}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def update_one_record(self, collection_name: str, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """PUT /{version}/database/collections/{collectionName}/{id}""" + return await self._transport.send( + target="hub", + path="/{version}/database/collections/{collectionName}/{id}", + method="PUT", + path_params={"collectionName": collection_name, "id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) diff --git a/tests/api/test_database.py b/tests/api/test_database.py index 92d4881..76654de 100644 --- a/tests/api/test_database.py +++ b/tests/api/test_database.py @@ -1,6 +1,11 @@ from __future__ import annotations -from norbix_python import Norbix, NorbixError +import asyncio +from urllib.parse import parse_qs, urlparse + +import httpx + +from norbix_python import AsyncNorbix, Norbix, NorbixError from ..helpers import make_client @@ -152,3 +157,56 @@ def test_api_database_update_one_request_shape() -> None: assert transport.last_request['method'] == 'PUT' assert transport.last_request is not None assert transport.last_request['url'].startswith('https://') + + +# --- Database audit (2026-10): find_own and find_merged_term_tree --- + + +def test_api_database_find_own_hits_the_own_route_with_query_values() -> None: + client, transport = make_client() + client.api.database.find_own("products", pageSize=10) + + assert transport.last_request is not None + assert transport.last_request["method"] == "GET" + url = urlparse(transport.last_request["url"]) + assert url.netloc == "api.norbix.ai" + assert url.path == "/v2/database/collections/products/own" + assert parse_qs(url.query) == {"pageSize": ["10"]} + + +def test_api_database_find_merged_term_tree_hits_the_merged_tree_route() -> None: + client, transport = make_client() + client.api.database.find_merged_term_tree("services") + + assert transport.last_request is not None + assert transport.last_request["method"] == "GET" + url = urlparse(transport.last_request["url"]) + assert url.netloc == "api.norbix.ai" + assert url.path == "/v2/database/taxonomies/services/merged-tree" + + +def test_api_database_audit_methods_exist_on_the_async_module() -> None: + seen: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return httpx.Response(200, json={}) + + async def run() -> None: + client = AsyncNorbix( + project_id="test-project", + bearer_token="test-token", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) + try: + await client.api.database.find_own("products") + await client.api.database.find_merged_term_tree("services") + finally: + await client.aclose() + + asyncio.run(run()) + + assert [(r.method, urlparse(str(r.url)).path) for r in seen] == [ + ("GET", "/v2/database/collections/products/own"), + ("GET", "/v2/database/taxonomies/services/merged-tree"), + ] diff --git a/tests/hub/test_database.py b/tests/hub/test_database.py index 3d4b11e..d90645c 100644 --- a/tests/hub/test_database.py +++ b/tests/hub/test_database.py @@ -1,6 +1,16 @@ from __future__ import annotations -from norbix_python import Norbix, NorbixError +import asyncio +import inspect +import json +from collections.abc import Callable +from typing import Any +from urllib.parse import parse_qs, urlparse + +import httpx +import pytest + +from norbix_python import AsyncNorbix, Norbix, NorbixError from ..helpers import make_client @@ -369,3 +379,147 @@ def test_hub_database_reveal_managed_flex_connection_string_request_shape() -> N assert transport.last_request is not None assert transport.last_request['url'].startswith('https://') + +# --- Database audit (2026-10): Hub records, trees, list settings, embed, bundle --- +# +# The generated tests above check only the verb and the https scheme. The cases +# below check the fully resolved path of every method added in the database +# audit, that query values reach a GET and a JSON body reaches a write, and +# that the async module has the same methods. Every call goes to a capture +# transport; nothing leaves the process. + + +DB = "/v2/database" +COLL = "products" +REC = "rec_1" +SCHEMA = "sch_1" +TAX = "services" +AGG = "agg_1" + +DbCase = tuple[str, str, str, Callable[[Any], Any]] + +HUB_DB_AUDIT_CASES: list[DbCase] = [ + # taxonomies + ("get_database_taxonomy_tree", "GET", f"{DB}/taxonomies/tree", lambda m: m.get_database_taxonomy_tree()), + ("get_database_merged_term_tree", "GET", f"{DB}/taxonomies/{TAX}/merged-tree", lambda m: m.get_database_merged_term_tree(TAX)), + ("get_database_taxonomy_term_tree", "GET", f"{DB}/taxonomies/{TAX}/terms/tree", lambda m: m.get_database_taxonomy_term_tree(TAX)), + # schemas + ("apply_database_schema_bundle", "POST", f"{DB}/schemas/apply-bundle", lambda m: m.apply_database_schema_bundle()), + ("get_database_schema_list_settings", "GET", f"{DB}/schemas/{SCHEMA}/list-settings", lambda m: m.get_database_schema_list_settings(SCHEMA)), + ("update_database_schema_list_settings", "PUT", f"{DB}/schemas/{SCHEMA}/list-settings", lambda m: m.update_database_schema_list_settings(SCHEMA)), + ("update_database_schema_embed", "PUT", f"{DB}/schemas/{SCHEMA}/embed", lambda m: m.update_database_schema_embed(SCHEMA)), + # collections / records + ("seed_collection_records", "POST", f"{DB}/collections/seed", lambda m: m.seed_collection_records()), + ("find_records", "GET", f"{DB}/collections/{COLL}", lambda m: m.find_records(COLL)), + ("insert_record", "POST", f"{DB}/collections/{COLL}", lambda m: m.insert_record(COLL)), + ("find_one_record", "GET", f"{DB}/collections/{COLL}/{REC}", lambda m: m.find_one_record(COLL, REC)), + ("update_one_record", "PUT", f"{DB}/collections/{COLL}/{REC}", lambda m: m.update_one_record(COLL, REC)), + ("delete_record", "DELETE", f"{DB}/collections/{COLL}/{REC}", lambda m: m.delete_record(COLL, REC)), + ("replace_record", "PUT", f"{DB}/collections/{COLL}/{REC}/replace", lambda m: m.replace_record(COLL, REC)), + ("change_record_responsibility", "PUT", f"{DB}/collections/{COLL}/{REC}/responsibility", lambda m: m.change_record_responsibility(COLL, REC)), + ("insert_many_records", "POST", f"{DB}/collections/{COLL}/many", lambda m: m.insert_many_records(COLL)), + ("update_many_records", "PUT", f"{DB}/collections/{COLL}/many", lambda m: m.update_many_records(COLL)), + ("delete_many_records", "DELETE", f"{DB}/collections/{COLL}/many", lambda m: m.delete_many_records(COLL)), + ("count_records", "GET", f"{DB}/collections/{COLL}/count", lambda m: m.count_records(COLL)), + ("distinct_record_values", "GET", f"{DB}/collections/{COLL}/distinct", lambda m: m.distinct_record_values(COLL)), + ("get_collection_indexes", "GET", f"{DB}/collections/{COLL}/indexes", lambda m: m.get_collection_indexes(COLL)), + ("aggregate_records", "POST", f"{DB}/collections/{COLL}/aggregate", lambda m: m.aggregate_records(COLL)), + ("execute_records_aggregate", "POST", f"{DB}/collections/{COLL}/aggregates/{AGG}/execute", lambda m: m.execute_records_aggregate(COLL, AGG)), +] + + +@pytest.mark.parametrize( + ("name", "verb", "path", "call"), + HUB_DB_AUDIT_CASES, + ids=[case[0] for case in HUB_DB_AUDIT_CASES], +) +def test_hub_database_audit_method_hits_the_gateway_route( + name: str, verb: str, path: str, call: Callable[[Any], Any] +) -> None: + client, transport = make_client(account_id=None) + call(client.hub.database) + + assert transport.last_request is not None + assert transport.last_request["method"] == verb + url = urlparse(transport.last_request["url"]) + assert url.netloc == "hub.norbix.ai" + assert url.path == path, f"{name}: got {transport.last_request['url']}, expected the path {path}" + + +def test_hub_database_audit_surface_size() -> None: + """23 Hub database routes were missing before the audit; all of them are covered here.""" + assert len(HUB_DB_AUDIT_CASES) == 23 + assert len({case[0] for case in HUB_DB_AUDIT_CASES}) == 23 + + +def test_hub_database_find_records_sends_query_values() -> None: + client, transport = make_client() + client.hub.database.find_records(COLL, filter='{"status":"active"}', pageSize=5, pageNumber=2) + + assert transport.last_request is not None + url = urlparse(transport.last_request["url"]) + assert url.path == f"{DB}/collections/{COLL}" + assert parse_qs(url.query) == { + "filter": ['{"status":"active"}'], + "pageSize": ["5"], + "pageNumber": ["2"], + } + assert transport.last_request["body"] == "" + + +def test_hub_database_insert_record_sends_the_document_in_the_json_body() -> None: + client, transport = make_client() + client.hub.database.insert_record(COLL, document='{"title":"Lamp","price":12}') + + assert transport.last_request is not None + assert urlparse(transport.last_request["url"]).query == "" + assert json.loads(transport.last_request["body"]) == {"document": '{"title":"Lamp","price":12}'} + + +def test_hub_database_update_schema_list_settings_keeps_the_id_in_the_path_only() -> None: + client, transport = make_client() + client.hub.database.update_database_schema_list_settings(SCHEMA, settings={"columns": ["title", "price"]}) + + assert transport.last_request is not None + assert urlparse(transport.last_request["url"]).path == f"{DB}/schemas/{SCHEMA}/list-settings" + assert json.loads(transport.last_request["body"]) == {"settings": {"columns": ["title", "price"]}} + + +def test_hub_database_record_call_sends_auth_and_project_headers() -> None: + client, transport = make_client() + client.hub.database.count_records(COLL) + + assert transport.last_request is not None + headers = transport.last_request["headers"] + assert headers["authorization"] == "Bearer test-token" + assert headers["x-cm-projectid"] == "test-project" + + +def test_hub_database_audit_methods_exist_on_the_async_module() -> None: + seen: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return httpx.Response(200, json={}) + + async def run() -> None: + client = AsyncNorbix( + project_id="test-project", + bearer_token="test-token", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) + try: + module = client.hub.database + for name, _, _, _ in HUB_DB_AUDIT_CASES: + assert inspect.iscoroutinefunction(getattr(module, name)), name + await module.delete_record(COLL, REC) + await module.get_database_merged_term_tree(TAX) + finally: + await client.aclose() + + asyncio.run(run()) + + assert [(r.method, urlparse(str(r.url)).path) for r in seen] == [ + ("DELETE", f"{DB}/collections/{COLL}/{REC}"), + ("GET", f"{DB}/taxonomies/{TAX}/merged-tree"), + ]