From 8b195f28ab20294844f0eaeb711fc1c8ba51c430 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 25 May 2026 06:23:07 -0600 Subject: [PATCH 1/3] Correct Resource post/put/patch data type; document search-sort + content-type behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `data` parameter on `post` / `put` / `patch` is the deserialized request body, not a `Promise` — the example showing `let data = await promisedData;` is misleading because the deserializer returns synchronously for every built-in content type. Update the signatures and the example. * Add a content-type → `data`-shape table so users know what to expect on non-JSON requests. * Document the indexed-attribute + matching-condition requirement for `sort` and give the workaround that returned the only error message users see today: "X is not indexed and not combined with any other conditions". Tracks harper #774 (long-fetch abort docs) and #773 (sort error). --- reference/resources/resource-api.md | 37 +++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 5e74eea6..e44b5c06 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -81,7 +81,7 @@ Performs a query on the resource or table. This is called by `get()` on collecti --- -### `put(target: RequestTarget | Id, data: Promise, context?: Resource | Context): Promise | Response` +### `put(target: RequestTarget | Id, data: object, context?: Resource | Context): Promise | Response` ### `put(record: object, context?): Promise` @@ -105,7 +105,7 @@ class MyResource extends Resource { --- -### `patch(target: RequestTarget | Id, data: Promise, context?: Resource | Context): Promise | Response` +### `patch(target: RequestTarget | Id, data: object, context?: Resource | Context): Promise | Response` Writes a partial record to the table, merging `data` into the existing record. @@ -127,14 +127,15 @@ class MyResource extends Resource { --- -### `post(target: RequestTarget, data: Promise, context?: Resource | Context): Promise | Response` +### `post(target: RequestTarget, data: object, context?: Resource | Context): Promise | Response` Called for HTTP POST requests. The default behavior creates a new record, but it can be overridden to implement custom actions. Prefer more explicit methods like `create()` or `update()` over calling `post` directly. +`data` is the already-deserialized request body. For an `application/json` request body it is the parsed JSON value (typically an object), so you can read fields directly — `await` is not required: + ```javascript class MyResource extends Resource { - static async post(target, promisedData) { - let data = await promisedData; + static async post(target, data) { if (data.action === 'create') { return this.create(target, data.content); } else if (data.action === 'update') { @@ -146,6 +147,17 @@ class MyResource extends Resource { } ``` +The exact shape of `data` depends on the request's `Content-Type`. The built-in deserializers produce: + +| `Content-Type` | `data` | +|---|---| +| `application/json` | parsed JSON value | +| `application/cbor`, `application/msgpack` | decoded value | +| `application/x-ndjson` | array of parsed lines | +| `text/plain` | string | + +Custom content types registered through `contentTypes.set(...)` receive whatever value the deserializer returns. + --- ### `delete(target: RequestTarget | Id, context?): Promise` @@ -661,6 +673,21 @@ Sort order object: | `descending` | Sort descending if `true` (default: `false`) | | `next` | Secondary sort to resolve ties (same structure) | +The sort `attribute` must be `@indexed` **and** narrowed by a `conditions` entry on the same attribute — Harper's query optimizer uses indexes to provide order, and refuses an unconditional ordered scan even on `@primaryKey`. A query with `sort` but no matching condition will throw: + +> `HdbError: is not indexed and not combined with any other conditions` + +To iterate a whole table in primary-key order, combine `sort` with an open-ended range condition on the same attribute: + +```javascript +Product.search({ + conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }], + sort: { attribute: 'id' }, +}); +``` + +If you just need to walk every record and order doesn't matter, omit `sort` entirely — `search({})` will iterate without an index requirement. + ### `explain` If `true`, returns conditions reordered as Harper will execute them (for debugging and optimization). From f2207e65c8a77200a57abc0b0d20998ddd3fded0 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Fri, 26 Jun 2026 06:34:35 -0600 Subject: [PATCH 2/3] Fix Resource data type (Promise) and search-sort condition rules The previous revision regressed two facts: - post/put/patch receive `data` as a Promise (REST.ts deserializes the body via the streaming deserializer, which returns a promise). Reading fields without `await` yields undefined. Restore the Promise signature and the `await data` in every example; also fix the put/patch examples that mixed `await data` with un-awaited `data.status`. - sort does not require a same-attribute condition. An `@indexed` sort attribute needs no condition at all; a non-indexed attribute only needs at least one condition on *any* attribute. Only (non-indexed attribute + zero conditions) throws. Document `allowFullScan` and clarify the bare `@primaryKey` case. Also correct the content-type table: msgpack is `application/x-msgpack`, and ndjson is registered as both `application/x-ndjson` and `application/ndjson`. Co-Authored-By: Claude Opus 4.8 (1M context) --- reference/resources/resource-api.md | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index e44b5c06..6d6235ec 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -81,7 +81,7 @@ Performs a query on the resource or table. This is called by `get()` on collecti --- -### `put(target: RequestTarget | Id, data: object, context?: Resource | Context): Promise | Response` +### `put(target: RequestTarget | Id, data: Promise, context?: Resource | Context): Promise | Response` ### `put(record: object, context?): Promise` @@ -98,14 +98,15 @@ This is called for HTTP PUT requests, and can be overridden to implement a custo ```javascript class MyResource extends Resource { static async put(target, data) { - return super.put(target, { ...(await data), status: data.status ?? 'active' }); + const record = await data; + return super.put(target, { ...record, status: record.status ?? 'active' }); } } ``` --- -### `patch(target: RequestTarget | Id, data: object, context?: Resource | Context): Promise | Response` +### `patch(target: RequestTarget | Id, data: Promise, context?: Resource | Context): Promise | Response` Writes a partial record to the table, merging `data` into the existing record. @@ -118,7 +119,8 @@ This is called for HTTP PATCH requests, and can be overridden to implement a cus ```javascript class MyResource extends Resource { static async patch(target, data) { - return super.patch(target, { ...(await data), status: data.status ?? 'active' }); + const record = await data; + return super.patch(target, { ...record, status: record.status ?? 'active' }); } } ``` @@ -127,15 +129,16 @@ class MyResource extends Resource { --- -### `post(target: RequestTarget, data: object, context?: Resource | Context): Promise | Response` +### `post(target: RequestTarget, data: Promise, context?: Resource | Context): Promise | Response` Called for HTTP POST requests. The default behavior creates a new record, but it can be overridden to implement custom actions. Prefer more explicit methods like `create()` or `update()` over calling `post` directly. -`data` is the already-deserialized request body. For an `application/json` request body it is the parsed JSON value (typically an object), so you can read fields directly — `await` is not required: +`data` is a promise that resolves to the deserialized request body — the body is read and decoded lazily, so you must `await` `data` before reading its fields. Once awaited, an `application/json` body resolves to the parsed JSON value (typically an object): ```javascript class MyResource extends Resource { static async post(target, data) { + data = await data; if (data.action === 'create') { return this.create(target, data.content); } else if (data.action === 'update') { @@ -147,13 +150,13 @@ class MyResource extends Resource { } ``` -The exact shape of `data` depends on the request's `Content-Type`. The built-in deserializers produce: +The shape of the resolved value depends on the request's `Content-Type`. The built-in deserializers produce: -| `Content-Type` | `data` | +| `Content-Type` | resolved `data` | |---|---| | `application/json` | parsed JSON value | -| `application/cbor`, `application/msgpack` | decoded value | -| `application/x-ndjson` | array of parsed lines | +| `application/cbor`, `application/x-msgpack` | decoded value | +| `application/x-ndjson`, `application/ndjson` | array of parsed lines | | `text/plain` | string | Custom content types registered through `contentTypes.set(...)` receive whatever value the deserializer returns. @@ -673,11 +676,18 @@ Sort order object: | `descending` | Sort descending if `true` (default: `false`) | | `next` | Secondary sort to resolve ties (same structure) | -The sort `attribute` must be `@indexed` **and** narrowed by a `conditions` entry on the same attribute — Harper's query optimizer uses indexes to provide order, and refuses an unconditional ordered scan even on `@primaryKey`. A query with `sort` but no matching condition will throw: +Harper uses an index to provide sort order, so a `sort` needs one of: + +- An `@indexed` sort `attribute`. Harper aligns the scan with the index automatically — **no condition is required**, and the condition (if any) does not have to be on the sort attribute. +- At least one `conditions` entry (on **any** attribute) when the sort `attribute` is not indexed. Harper filters by the condition and then orders the result set in memory. + +Only the combination of a non-indexed sort `attribute` **and** zero conditions is rejected: > `HdbError: is not indexed and not combined with any other conditions` -To iterate a whole table in primary-key order, combine `sort` with an open-ended range condition on the same attribute: +Note the bare `@primaryKey` is treated as not indexed for this purpose (it has its own primary store rather than a secondary index), so sorting by the primary key alone — with no conditions — hits this error. + +To iterate a whole table in primary-key order, add an open-ended range condition (which can be on the primary key or any other attribute): ```javascript Product.search({ @@ -686,7 +696,7 @@ Product.search({ }); ``` -If you just need to walk every record and order doesn't matter, omit `sort` entirely — `search({})` will iterate without an index requirement. +Alternatively, pass `allowFullScan: true` to permit an unconditional ordered scan, or — if order doesn't matter — omit `sort` entirely and `search({})` will iterate without an index requirement. ### `explain` From 02f417657d747b4c594953fef24ba22a1981a629 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Fri, 26 Jun 2026 06:47:40 -0600 Subject: [PATCH 3/3] Format resource-api.md table with prettier Co-Authored-By: Claude Opus 4.8 (1M context) --- reference/resources/resource-api.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 6d6235ec..6e222ed4 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -152,12 +152,12 @@ class MyResource extends Resource { The shape of the resolved value depends on the request's `Content-Type`. The built-in deserializers produce: -| `Content-Type` | resolved `data` | -|---|---| -| `application/json` | parsed JSON value | -| `application/cbor`, `application/x-msgpack` | decoded value | +| `Content-Type` | resolved `data` | +| -------------------------------------------- | --------------------- | +| `application/json` | parsed JSON value | +| `application/cbor`, `application/x-msgpack` | decoded value | | `application/x-ndjson`, `application/ndjson` | array of parsed lines | -| `text/plain` | string | +| `text/plain` | string | Custom content types registered through `contentTypes.set(...)` receive whatever value the deserializer returns.