docs(rest): map exported tables to their automatic REST endpoints - #650
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the documentation to clarify the requirements for serving tables over REST, specifically noting that both the @export directive and the rest plugin must be enabled. It also introduces a comprehensive section detailing the automatic REST endpoints generated for exported tables. The feedback suggests replacing em dashes with hyphens in the newly added Markdown text to maintain consistency with the repository's style guidelines.
|
|
||
| The optional `name` parameter specifies the URL path segment (e.g., `/my-table/`). Without `name`, the type name is used. | ||
|
|
||
| `@export` alone does not serve HTTP traffic — the `rest` plugin must also be enabled in the application's `config.yaml`. See [REST Overview / Tables and Their Automatic Endpoints](../rest/overview.md#tables-and-their-automatic-endpoints) for the endpoints an exported table produces and the `rest: true` requirement. |
There was a problem hiding this comment.
Per the repository's general rules, please use a hyphen (-) instead of an em dash (—) for consistency. The rule states to use hyphens even if there are pre-existing inconsistencies in the file.
| `@export` alone does not serve HTTP traffic — the `rest` plugin must also be enabled in the application's `config.yaml`. See [REST Overview / Tables and Their Automatic Endpoints](../rest/overview.md#tables-and-their-automatic-endpoints) for the endpoints an exported table produces and the `rest: true` requirement. | |
| `@export` alone does not serve HTTP traffic - the `rest` plugin must also be enabled in the application's `config.yaml`. See [REST Overview / Tables and Their Automatic Endpoints](../rest/overview.md#tables-and-their-automatic-endpoints) for the endpoints an exported table produces and the `rest: true` requirement. |
References
- In Markdown documentation, use hyphens ('-') instead of em dashes ('—') as field separators to adhere to the style guide, even if the file has pre-existing inconsistencies using em dashes.
There was a problem hiding this comment.
Not taking this one. The em-dash rule is being over-generalized here.
AGENTS.md says em dashes "are fine for parenthetical asides and match existing prose; do not use them as field separators." The field-separator prohibition refers to the bullet immediately above it - Type: / Default: reference blocks, where each field goes on its own line. It is not a ban on - **term** — description in prose lists.
Two checks: the docs tree has 633 em-dash list-item lines across 63 reference files, and AGENTS.md itself uses this exact pattern - including in the rule being quoted (- **Hyphens vs. dashes** — em dashes...).
So these are the established convention, not "pre-existing inconsistencies." Applying the suggestion would leave the touched lines inconsistent with every sibling line in the same list.
sent with Claude Opus 5
| rest: true | ||
| ``` | ||
|
|
||
| Neither half is sufficient on its own. Without `@export` the table has no REST route and callers get `404`. Without `rest: true` the REST handler is never registered for the application, so exported tables do not respond to HTTP requests at all — `rest` is not enabled by default. |
There was a problem hiding this comment.
Per the repository's general rules, please use a hyphen (-) instead of an em dash (—). This change should be applied consistently throughout the new content in this file where em dashes are used (e.g., on lines 67, 72, 73, and 75).
| Neither half is sufficient on its own. Without `@export` the table has no REST route and callers get `404`. Without `rest: true` the REST handler is never registered for the application, so exported tables do not respond to HTTP requests at all — `rest` is not enabled by default. | |
| Neither half is sufficient on its own. Without `@export` the table has no REST route and callers get `404`. Without `rest: true` the REST handler is never registered for the application, so exported tables do not respond to HTTP requests at all - `rest` is not enabled by default. |
References
- In Markdown documentation, use hyphens ('-') instead of em dashes ('—') as field separators to adhere to the style guide, even if the file has pre-existing inconsistencies using em dashes.
There was a problem hiding this comment.
Not taking this one. The em-dash rule is being over-generalized here.
AGENTS.md says em dashes "are fine for parenthetical asides and match existing prose; do not use them as field separators." The field-separator prohibition refers to the bullet immediately above it - Type: / Default: reference blocks, where each field goes on its own line. It is not a ban on - **term** — description in prose lists.
Two checks: the docs tree has 633 em-dash list-item lines across 63 reference files, and AGENTS.md itself uses this exact pattern - including in the rule being quoted (- **Hyphens vs. dashes** — em dashes...).
So these are the established convention, not "pre-existing inconsistencies." Applying the suggestion would leave the touched lines inconsistent with every sibling line in the same list.
sent with Claude Opus 5
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-650 This preview will update automatically when you push new commits. |
dd9bc32 to
d4687e6
Compare
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-650 This preview will update automatically when you push new commits. |
kriszyp
left a comment
There was a problem hiding this comment.
This is a good overview/intro, I like it.
🤖 Reviewed with Codex
|
|
||
| ## Tables and Their Automatic Endpoints | ||
|
|
||
| A table is served over REST only when **both** of the following are true: |
There was a problem hiding this comment.
This is too absolute for the documented table-extension path: reference/resources/overview.md:32-42 explicitly tells users to omit @export when a JavaScript subclass of tables.MyTable owns the route, with rest: true at reference/resources/overview.md:74-82. Following this new rule can make those users add both exports and create the conflicting endpoints that the existing guide warns about. Could we scope this sentence and reference/rest/overview.md:61 to the automatically registered/default table Resource?
There was a problem hiding this comment.
Good catch — scoped it. The section now opens by saying it describes the default table Resource that Harper registers automatically, and the "neither half is sufficient" paragraph is worded the same way. I also added a paragraph pointing at reference/resources/overview.md "Extending a Table": @export is how the table itself claims the URL, so when a tables.MyTable subclass should own it you omit @export and export the class instead.
sent with Claude Opus 5
| | `GET /Product/` | The record collection. Append query parameters to search, filter, sort, and page. | [GET](#get), [Querying](./querying.md) | | ||
| | `GET /Product/{id}` | A single record by primary key; `404` when no such record exists. | [GET](#get) | | ||
| | `GET /Product/{id}.property` | A single property of one record. Only properties declared in the schema. | [GET](#get) | | ||
| | `POST /Product/` | Creates a record with a Harper-assigned primary key and responds `201`. Requires the trailing slash — `POST /Product` returns `404`. | [POST](#post) | |
There was a problem hiding this comment.
A collection POST does not always use a Harper-assigned key. If the request body supplies the table's primary-key property, Harper preserves that value and returns it in Location; a key is generated only when the property is omitted. Could this say “with a Harper-assigned primary key when one is not supplied” so clients do not expect their explicit key to be replaced?
There was a problem hiding this comment.
Confirmed and fixed. Table.create does let id = requestTargetToId(target) ?? record[primaryKey] and only calls getNewId() when that is undefined, so the row now reads "Creates a record and responds 201, with a Harper-assigned primary key when the body does not supply one." I added a note that a supplied key is preserved and echoed in Location, and that posting an existing key returns 409 rather than overwriting — and updated the ### POST prose below, which had the same "without specifying a primary key" claim.
sent with Claude Opus 5
| - `HEAD` is served exactly as `GET` with the response body omitted. `QUERY` is accepted on the collection path (`QUERY /Product/`) and runs a search taken from the request body rather than the URL. | ||
| - On a successful `POST`, the new record's primary key is returned in the `Location` response header. The header carries the bare key value, not a URL — it is not a link to follow. | ||
| - `PUT` replaces the stored record, with three exceptions that Harper always applies: a [`@createdTime`](../database/schema.md#createdtime) attribute keeps the original record's value, an [`@updatedTime`](../database/schema.md#updatedtime) attribute is re-stamped with the time of the write, and the primary key is forced to match the `{id}` in the URL even if the body carries a different one. | ||
| - Enabling `rest` also enables [WebSocket](./websockets.md) and [Server-Sent Events](./server-sent-events.md) subscriptions on these same resource paths. |
There was a problem hiding this comment.
This is not unconditional for WebSockets: the configuration example at reference/rest/overview.md:33-35 explicitly supports webSocket: false, which leaves REST enabled while suppressing WebSocket registration. Could this say that REST enables WebSocket subscriptions by default, unless webSocket is disabled, while describing SSE separately?
There was a problem hiding this comment.
Fixed, and split into two bullets. WebSocket is now "by default", with webSocket: false under rest suppressing the registration while REST keeps serving — that matches server/REST.ts, where the early return sits after the HTTP handler is registered and before scope.server.ws(...). SSE gets its own bullet: it is negotiated per request via Accept: text/event-stream inside the HTTP handler, so the webSocket option does not affect it.
sent with Claude Opus 5
| - On a successful `POST`, the new record's primary key is returned in the `Location` response header. The header carries the bare key value, not a URL — it is not a link to follow. | ||
| - `PUT` replaces the stored record, with three exceptions that Harper always applies: a [`@createdTime`](../database/schema.md#createdtime) attribute keeps the original record's value, an [`@updatedTime`](../database/schema.md#updatedtime) attribute is re-stamped with the time of the write, and the primary key is forced to match the `{id}` in the URL even if the body carries a different one. | ||
| - Enabling `rest` also enables [WebSocket](./websockets.md) and [Server-Sent Events](./server-sent-events.md) subscriptions on these same resource paths. | ||
| - Every exported resource is included in the generated [OpenAPI](#openapi) document. |
There was a problem hiding this comment.
static hidden = true drops a programmatic Resource from OpenAPI (reference/resources/resource-api.md:612-614), and type-level @hidden likewise suppresses table resources from the OpenAPI document (reference/database/schema.md:244-246). Please qualify this as “Every non-hidden exported resource” or otherwise call out the exception.
There was a problem hiding this comment.
Qualified it. The bullet now reads "Every non-hidden exported resource is included in the generated OpenAPI document" and calls out both exceptions explicitly, linking @hidden on the type and static hidden = true on a programmatic Resource.
sent with Claude Opus 5
|
|
||
| The optional `name` parameter specifies the URL path segment (e.g., `/my-table/`). Without `name`, the type name is used. | ||
|
|
||
| `@export` alone does not serve HTTP traffic — the `rest` plugin must also be enabled in the application's `config.yaml`. See [REST Overview / Tables and Their Automatic Endpoints](../rest/overview.md#tables-and-their-automatic-endpoints) for the endpoints an exported table produces and the `rest: true` requirement. |
There was a problem hiding this comment.
The no-configuration-file exception documented at reference/rest/overview.md:63-66 makes this statement false for a bare component directory: Harper's built-in default enables both rest and *.graphql, so the user does not need a config.yaml. Could this say that REST must be enabled either explicitly or by the built-in no-config default?
There was a problem hiding this comment.
You're right, that contradicted the carve-out I added on the other page. It now says REST must be enabled for the application "either explicitly with rest: true in its config.yaml or by Harper's built-in default for a component directory that has no configuration file at all." I made the matching change to requirement 2 in the REST overview so the two pages agree.
sent with Claude Opus 5
Adds a "Tables and Their Automatic Endpoints" section to the REST overview that joins the two facts a reader currently has to assemble from separate pages: a table needs `@table @export` in the schema AND `rest: true` in the application's config.yaml before it answers any HTTP request. `rest` is not in defaultConfig.yaml, so without it the REST handler is never registered and exported tables 404. The section is a compact endpoint table whose rows link down to the existing per-method sections rather than restating them, so PUT/PATCH semantics stay defined in exactly one place. Also adds a one-sentence pointer from the schema reference's `@export` directive to the new section. Closes #538 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…T status
Three corrections to the endpoints section, each re-verified against
harper origin/main c4dd96237:
- Remove the incorrect HEAD/OPTIONS/405 note. `allowedMethods()`
(resources/Resource.ts:1003) is called with the Resource *class* in
server/REST.ts's OPTIONS case, so it inspects statics, not instance
methods: it over-reports MOVE and COPY (statics exist and dispatch to
instance methods a table does not define, so both 405) and omits HEAD
(no static). "OPTIONS reports the supported methods" overstated it.
Replaced with what is actually true: HEAD is GET minus the body
(REST.ts:352), and QUERY works on the collection path, dispatching
through `Resource.query` to the table's `search` (Resource.ts:347).
- Add a carve-out for components with no configuration file.
static/defaultConfig.yaml has no `rest` key, but
components/DEFAULT_CONFIG.ts sets `rest: true`, and
componentLoader.ts:602-614 selects one or the other verbatim with no
merge. The two cases are disjoint, so the blanket "not enabled by
default" told a reader with no config.yaml their exported tables would
404 when they work.
- Correct the POST row: `POST /Table` without the trailing slash returns
404 from a purpose-built ClientError during argument normalization
(Resource.ts:747), not 405; only `POST /Table/{id}` reaches
missingMethod. Also note the response is 201 and that `Location`
carries the bare primary key rather than a URL (Resource.ts:246,
REST.ts:319-320).
Also folds in PUT's three always-applied exceptions to full replacement
(Table.ts:2461-2487) as a note rather than a table row, so the
"omitted properties are removed" warning keeps the lede.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ptions Addresses review feedback on the "Tables and Their Automatic Endpoints" section: - Scope the "@export + rest" requirement to the default table Resource that Harper registers automatically, and point at Extending a Table for the case where a tables.MyTable subclass owns the route instead (omit @export). The previous absolute wording contradicted reference/resources/overview.md and could lead users into the conflicting-endpoints trap it warns about. - A collection POST only generates a primary key when the body omits the primary-key property. A supplied key is preserved and echoed in Location; a POST to an existing key returns 409 rather than overwriting. - REST enables WebSocket subscriptions by default, not unconditionally: rest.webSocket: false suppresses WS registration while REST keeps serving. SSE is described separately, since it is negotiated per request via Accept and is not gated by that option. - Qualify OpenAPI inclusion as every non-hidden exported resource, naming both @hidden on the type and static hidden = true on a programmatic Resource. - reference/database/schema.md no longer claims config.yaml is required; it now matches the no-configuration-file carve-out documented on the REST page. Verified against harper origin/main: Table.create (resources/Table.ts), Resource.create/post (resources/Resource.ts), the webSocket early return and the SSE branch (server/REST.ts), and the hidden filters (resources/openApi.ts). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d4687e6 to
3baf519
Compare
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-650 This preview will update automatically when you push new commits. |
🧹 Preview CleanupThe preview deployment for this PR has been removed. |
What
Adds a canonical Tables and Their Automatic Endpoints section to
reference/rest/overview.md, plus a one-sentence pointer from the@exportdirective inreference/database/schema.md.Why this shape
The facts already existed but were never joined:
@exportis documented in the schema reference,rest: trueunder Configuration in the REST overview, and the per-method behavior in that same file's GET/PUT/POST/PATCH/DELETE sections. Nothing connected "I defined and exported a table" to "here is the HTTP surface I now have." That disconnect is the reported gap.The new section is a compact endpoint table whose rows link down to the existing per-method sections rather than restating them. Restating PUT/PATCH semantics in two places on the same page would guarantee drift, and PUT/PATCH are exactly the two whose nuance matters most. Each row carries a one-line description and an anchor to the fuller section below.
It lives in
rest/overview.mdrather thanschema.mdbecauserest: trueand all per-method nuance are already on that page.schema.mdgets a pointer, not a duplicate table.The
rest: truegateThe section states plainly that both halves are required:
@table @exportin the schema - without it there is no REST route and callers get404.rest: truein the application'sconfig.yaml- without it the REST handler is never registered for the application, so even an exported table does not respond to HTTP.With one carve-out, called out in an admonition: a component directory with no configuration file at all falls back to
components/DEFAULT_CONFIG.ts, which setsrest: trueand loads*.graphqlfrom the component root.componentLoader.ts:602-614selects a config file or the built-in default verbatim, with no merge, which makes the two cases fully disjoint - so aconfig.yamlthat omitsrestturns REST off even though the same directory would have had it with no file present. That is the gotcha the issue centers on, stated without over-claiming.Verified against harper
origin/main(c4dd96237)rest: truegate.restis a key inTRUSTED_RESOURCE_PLUGINS(components/componentLoader.ts:300) mapping toserver/REST.ts; absent config keys are skipped by the loader.static/defaultConfig.yamlhas norestkey;components/DEFAULT_CONFIG.ts:2does setrest: true, and applies only when no config file exists (see carve-out above).server/REST.tsdispatchesGET/HEAD/POST/PUT/DELETE/PATCH/OPTIONS/CONNECT/TRACE/QUERY/COPY/MOVE, delegating to the matched Resource class's same-named static and returning405viamissingMethod()when the underlying instance method is absent. A table's working surface is GET, HEAD, PUT, POST, PATCH, DELETE, and QUERY.Table.put->update(..., fullUpdate = true); thefullUpdatebranch in_writeUpdatereplaces the stored record. Properties absent from the body are removed.Table.ts:2461-2487):@createdTimeretains the original record's value,@updatedTimeis re-stamped with the write time, and the primary key is forced to the URL{id}even when the body carries a different one. Recorded as a note rather than a table row so the "omitted properties are removed" warning keeps the lede.Table.patch->update(..., fullUpdate = false), a top-level merge. Row links to the existing detailed warning.201(REST.ts:319). The new key is returned inLocation, and the value is the bare primary key, not a URL -Resource.ts:246setscontext.newLocation = id ?? results?.[primaryKey]andREST.ts:320emits it verbatim.POST /Tablewithout the trailing slash returns 404 from a purpose-builtClientErrorduring argument normalization (Resource.ts:747); onlyPOST /Table/{id}falls through tomissingMethod-> 405.Resource.ts:347dispatchesmethod: 'query'toresource.search, whichTable.ts:3244implements. Exercised end-to-end over real HTTP inintegrationTests/security/query-row-allowread-checkpermission.test.ts(QUERY /Vault/-> 200), withunitTests/server/serverHelpers/uwsServer.test.jsconfirming the server accepts the verb.RequestTargetsetsisCollection = trueonly for a trailing slash (or a bare query string); an exact resource-path match setsisCollection = false, id = null.Table.getthen returns a describe object (table name, database,attributes, and an href to./) forGET /Table.recordCount/estimatedRecordRangeareundefinedunless expensive estimates are requested, so the section does not promise a record count.Table.deletetreats any collection target as a search target and deletes every matching record. With no query parameters that is every record in the table. The row says so accurately without making it sound casual.No place where the source contradicted the existing text of
rest/overview.md; the page was accurate on every point checked.Corrections made after initial review
Three problems were found in the first push and are fixed here:
HEAD/OPTIONS/405note. It claimedOPTIONS"reports the resource's supported methods." It does not.allowedMethods()(resources/Resource.ts:1003) iteratesKNOWN_METHODS=get, head, put, post, delete, patch, query, move, copyand pushes anytypeof resource[method] === 'function'- andREST.ts's OPTIONS case calls it with the Resource class, so it inspects statics rather than instance methods.Resourcedefines statics for every entry excepthead, so a table'sAllowheader over-reportsMOVEandCOPY(the statics exist and dispatch to instance methods a table does not define, so both 405) and omitsHEAD(served, but no static). Rather than document that mismatch, the note now states only what is true:HEADisGETminus the body (REST.ts:352), andQUERYruns a body-supplied search on the collection path.restis not enabled by default" would have told a reader with noconfig.yamlthat their exported tables 404 when they actually work.POSTis404, not405, and the two cases have different codes for different reasons (see above).Note for a follow-up skills PR
The hand-authored rule
harper-best-practices/rules/adding-tables-with-schemasin@harperfast/skillscontains a PUT-semantics error: it saysPUT /{Table}/{id}"Updates an existing record." PUT is create-or-replace with upsert semantics, and properties omitted from the body are removed - the difference between an update and silent data loss. The same rule also omits that PATCH's merge is shallow, and omits theLocationheader onPOST. That rule is out of scope here (separate repo, gated work); a follow-up skills PR will correct it, and can then source from this section.Separately, the
Allowmismatch found while checking correction 1 (MOVE/COPY advertised but 405, HEAD served but unadvertised) looks like a harper bug rather than something to document. Not filed here.Verification
mainatca6143ef.npm run format:writeclean,npm run format:checkpasses.npm run buildsucceeds. Every anchor added was verified by grepping the generatedbuild/reference/v5/**.htmlfor the targetid(rather than trusting the exit code, sinceonBrokenAnchorsonly warns):#export,#createdtime,#updatedtime,#configuration,#get,#post,#put,#patch,#delete,#url-structure,#openapi,#tables-and-their-automatic-endpoints, plus the four cross-page targets. The only broken anchors reported are the two known pre-existing ones (backups/overviewandrelease-notes/v5-lincoln/5.1), owned elsewhere.Closes #538
🤖 Generated with Claude Code