@harperfast/template-vue-ts-studio 1.10.0 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,8 +7,8 @@ metadata:
7
7
  - reference/v5/resources/overview.md#Custom External Data Source
8
8
  - reference/v5/resources/overview.md#Exporting Resources as Endpoints
9
9
  - reference/v5/components/javascript-environment.md#Module Loading
10
- sourceCommit: 4fe4c9c95e0974eaa77032f6f10e36fbd8ec64ac
11
- inputHash: 9b911fd8e036b808
10
+ sourceCommit: f37a8c4021e20d5c74c1d339a6b6c8c196b5603e
11
+ inputHash: df69870433c0b3e5
12
12
  ---
13
13
 
14
14
  # Custom Resources
@@ -17,17 +17,17 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
17
17
 
18
18
  ## When to Use
19
19
 
20
- Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering programmatic routes in a Harper application. Use it any time business logic needs to live outside a standard table-backed resource.
20
+ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed schema, or when a specific URL shape is required.
21
21
 
22
22
  ## How It Works
23
23
 
24
- 1. **Import `Resource` from the `harper` package**: Always import explicitly rather than relying on globals.
24
+ 1. **Import `Resource` from `harper`**: Always import from the `harper` package rather than relying on globals.
25
25
 
26
26
  ```javascript
27
27
  import { tables, Resource } from 'harper';
28
28
  ```
29
29
 
30
- 2. **Define a class that `extends Resource`**: Use `export class` so Harper can expose it as an endpoint. Implement HTTP methods as `static` methods on the class.
30
+ 2. **Define a class that `extends Resource`**: Implement HTTP methods as `static` methods. Each method receives a `target` object.
31
31
 
32
32
  ```javascript
33
33
  export class CustomEndpoint extends Resource {
@@ -39,7 +39,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
39
39
  }
40
40
  ```
41
41
 
42
- 3. **Add async `static` methods for each HTTP verb you need**: Methods receive `target` (contains `target.id`, etc.) and, for write operations, `data`.
42
+ 3. **Use `async` static methods for external calls**: Await fetch or other async operations inside `static` handlers.
43
43
 
44
44
  ```javascript
45
45
  export class MyExternalData extends Resource {
@@ -57,29 +57,46 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
57
57
  }
58
58
  ```
59
59
 
60
- 4. **Control the URL by choosing the export form**: The shape of the export determines the resulting URL path. Path matching is case-sensitive.
60
+ 4. **Export the class to create an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
61
61
 
62
- | Export form | URL | Notes |
63
- | ---------------------------------------- | --------------- | --------------------------------------------------------------- |
64
- | `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment. |
65
- | `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
66
- | `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
67
- | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration; useful when the path is dynamic. |
62
+ | Export form | URL | Notes |
63
+ | ------------------------------------------- | --------------- | --------------------------------------------------------------- |
64
+ | `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment. Case-sensitive. |
65
+ | `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
66
+ | `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
67
+ | `export { Foo as '/widget/:id' }` | `/widget/:id` | Rename the export to set the path directly. |
68
+ | `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
69
+ | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
68
70
 
69
- 5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the class.
71
+ URL path matching is case-sensitive `/Foo/` and `/foo/` are different endpoints.
72
+
73
+ 5. **Declare path parameters with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
74
+
75
+ ```javascript
76
+ export class Widget extends Resource {
77
+ static path = '/widget/:id/action/:action';
78
+ static get(target) {
79
+ return { id: target.id, action: target.action };
80
+ }
81
+ }
82
+ ```
83
+
84
+ A `static path` takes precedence over the export name. A leading `/` makes the path root-relative (top-level). A leading `./` or bare name resolves relative to the component directory.
85
+
86
+ 6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` when the path cannot be known at export time.
70
87
 
71
88
  ```javascript
72
89
  server.resources.set('my-path', Foo);
73
90
  ```
74
91
 
75
- 6. **Optionally use the resource as a cache source for a local table**: Pass the class to `sourcedFrom`.
92
+ 7. **Optionally source a table from a custom resource**: Use the resource as a caching layer for a local table.
76
93
  ```javascript
77
94
  tables.MyCache.sourcedFrom(MyExternalData);
78
95
  ```
79
96
 
80
97
  ## Examples
81
98
 
82
- Wrap an external API and expose it as an endpoint, then back a local cache table with it:
99
+ ### External API wrapper with GET and PUT
83
100
 
84
101
  ```javascript
85
102
  import { tables, Resource } from 'harper';
@@ -102,26 +119,47 @@ export class MyExternalData extends Resource {
102
119
  tables.MyCache.sourcedFrom(MyExternalData);
103
120
  ```
104
121
 
105
- Programmatic registration with a custom path:
122
+ ### Path parameters with `static path`
123
+
124
+ ```javascript
125
+ import { Resource } from 'harper';
126
+
127
+ export class Widget extends Resource {
128
+ // GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
129
+ static path = '/widget/:id/action/:action';
130
+ static get(target) {
131
+ return { id: target.id, action: target.action };
132
+ }
133
+ }
134
+
135
+ export class Files extends Resource {
136
+ // GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
137
+ static path = '/files/*rest';
138
+ static get(target) {
139
+ return { path: target.rest };
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Programmatic registration
106
145
 
107
146
  ```javascript
108
147
  import { Resource } from 'harper';
109
148
 
110
- export class CustomEndpoint extends Resource {
149
+ export class Foo extends Resource {
111
150
  static get(target) {
112
- return {
113
- data: doSomething(),
114
- };
151
+ return { data: doSomething() };
115
152
  }
116
153
  }
117
154
 
118
- server.resources.set('my-path', CustomEndpoint);
155
+ server.resources.set('my-path', Foo);
119
156
  ```
120
157
 
121
158
  ## Notes
122
159
 
123
- - `export class` directly produces a URL from the class name (e.g., `export class Foo extends Resource {}` `/Foo/`). Do not export the same resource from both a schema file and a JavaScript file — this creates conflicting exports.
124
- - URL path segments are case-sensitive: `/Foo/` and `/foo/` are different endpoints.
125
- - For CommonJS modules, use `const { tables, Resource } = require('harper');` instead of the ESM import.
126
- - When developing a component in its own directory, run `npm link harper` to ensure typings match your installed version. All installed components have `harper` automatically linked.
127
- - The `static` keyword is required on all HTTP verb methods Harper dispatches requests through static class methods, not instance methods.
160
+ - A bare `*` wildcard (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
161
+ - Resolution order: exact/static paths always win over parameterized ones. Among parameterized routes, more specific paths win — a literal segment beats `:param`, which beats `*`, compared left to right.
162
+ - Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as `{param}` URI templates.
163
+ - If a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
164
+ - Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
165
+ - Harper runs as a single process — `tables`, `databases`, and other APIs are the same live, process-wide objects regardless of which component accesses them.
@@ -2,177 +2,185 @@
2
2
  name: programmatic-table-requests
3
3
  description: How to interact with Harper tables programmatically using the `tables` object.
4
4
  metadata:
5
- mode: synthesized
5
+ mode: generate
6
+ sources:
7
+ - reference/v5/database/api.md#`tables`
8
+ - reference/v5/resources/resource-api.md#Query Object
9
+ - 'reference/v5/database/api.md#`transaction(context?, callback)`'
10
+ - >-
11
+ reference/v5/resources/resource-api.md#`update(target: RequestTarget | Id,
12
+ updates?: object): Promise<Resource>`
13
+ - >-
14
+ reference/v5/resources/resource-api.md#`addTo(property: string, value:
15
+ number)`
16
+ - reference/v5/components/javascript-environment.md#Module Loading
17
+ sourceCommit: be709f9978319dcdb669c05d794effc82bcda8b7
18
+ inputHash: 3d2417fbff687c42
6
19
  ---
7
20
 
8
21
  # Programmatic Table Requests
9
22
 
10
- Instructions for the agent to follow when interacting with Harper tables via code.
23
+ Instructions for the agent to interact with Harper tables programmatically using the `tables` object and its query API.
11
24
 
12
25
  ## When to Use
13
26
 
14
- Use this skill when you need to perform database operations (CRUD, search, subscribe) from within Harper Resources or scripts.
27
+ Apply this rule when writing server-side Harper code that reads from or writes to tables directly — for example, in request handlers, background jobs, or SSR entry points — instead of going through the REST API. Use it whenever you need to construct queries with `conditions`, paginate results, select specific fields, or perform CRDT-safe mutations with `addTo`.
15
28
 
16
29
  ## How It Works
17
30
 
18
- 1. **Access the Table**: Use the global `tables` object followed by your table name (e.g., `tables.MyTable`).
19
- 2. **Perform CRUD Operations**:
20
- - **Get**: `await tables.MyTable.get(id)` for a single record or `await tables.MyTable.get({ conditions: [...] })` for multiple.
21
- - **Create**: `await tables.MyTable.post(record)` (auto-generates ID) or `await tables.MyTable.put(id, record)`.
22
- - **Update**: `await tables.MyTable.patch(id, partialRecord)` for partial updates.
23
- - **Delete**: `await tables.MyTable.delete(id)`.
24
- 3. **Use Updatable Records for Atomic Ops**: Call `update(id)` to get a reference, then use `addTo` or `subtractFrom` for atomic increments/decrements:
25
- ```typescript
26
- const stats = await tables.Stats.update('daily');
27
- stats.addTo('viewCount', 1);
28
- ```
29
- 4. **Search and Stream**: Use `search(query)` for efficient streaming of large result sets:
30
- ```typescript
31
- for await (const record of tables.MyTable.search({ conditions: [...] })) {
32
- // process record
33
- }
34
- ```
35
- See the [Query Conditions](#query-conditions) section below for the full query object reference.
36
- 5. **Real-time Subscriptions**: Use `subscribe(query)` to listen for changes:
37
- ```typescript
38
- for await (const event of tables.MyTable.subscribe(query)) {
39
- // handle event
40
- }
41
- ```
42
- 6. **Publish Events**: Use `publish(id, message)` to trigger subscriptions without necessarily persisting data.
43
-
44
- ## Query Conditions
45
-
46
- When passing a query to `search()`, `get()`, or `subscribe()`, use a query object with a `conditions` array.
47
-
48
- ### Condition Object Shape
31
+ 1. **Import `tables`**: Import from the `harper` package. Each table defined in `schema.graphql` with `@table` is available as a named property.
49
32
 
50
- | Property | Description |
51
- | ------------ | ------------------------------------------------------------------------------------------ |
52
- | `attribute` | Field name, or array of field names to traverse a relationship (e.g., `['brand', 'name']`) |
53
- | `value` | The value to compare against |
54
- | `comparator` | One of the comparator strings below (default: `equals`) |
55
- | `operator` | `and` (default) or `or` — applies to a nested `conditions` block |
56
- | `conditions` | Nested array of condition objects for complex AND/OR logic |
57
-
58
- ### Comparator Values
33
+ ```javascript
34
+ import { tables } from 'harper';
35
+ const { Product } = tables;
36
+ // same as: databases.data.Product
37
+ ```
59
38
 
60
- Use these exact strings incorrect comparator names will silently fail or error:
39
+ 2. **Define your schema**: Declare tables in `schema.graphql` using `@table` and `@primaryKey`.
61
40
 
62
- | Comparator | Meaning |
63
- | -------------------- | ---------------------------------------------------------- |
64
- | `equals` | Exact match (default) |
65
- | `not_equal` | Not equal |
66
- | `greater_than` | `>` |
67
- | `greater_than_equal` | `>=` |
68
- | `less_than` | `<` |
69
- | `less_than_equal` | `<=` |
70
- | `starts_with` | String starts with value |
71
- | `contains` | String contains value |
72
- | `ends_with` | String ends with value |
73
- | `between` | Value is between two bounds (pass `value` as `[min, max]`) |
41
+ ```graphql
42
+ # schema.graphql
43
+ type Product @table {
44
+ id: Long @primaryKey
45
+ name: String
46
+ price: Float
47
+ }
48
+ ```
74
49
 
75
- ### Query Object Parameters
50
+ 3. **Create and mutate records**: Use `create`, `patch`, `get`, and `update` on the table class.
76
51
 
77
- | Property | Description |
78
- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79
- | `conditions` | Array of condition objects |
80
- | `limit` | Maximum number of records to return |
81
- | `offset` | Number of records to skip (for pagination) |
82
- | `select` | Array of fields to return: attribute names (supports `$id` and `$updatedtime`), or an object `{ name, select }` to include fields from a related record — see [Selecting Related Data](#selecting-related-data) |
83
- | `sort` | Object with `attribute`, `descending` (bool), and optional `next` for secondary sort |
52
+ ```javascript
53
+ // Create a new record (id auto-generated)
54
+ const created = await Product.create({ name: 'Shirt', price: 9.5 });
84
55
 
85
- ### Examples
56
+ // Modify the record
57
+ await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
86
58
 
87
- **Simple filter:**
59
+ // Retrieve by primary key
60
+ const record = await Product.get(created.id);
61
+ ```
88
62
 
89
- ```javascript
90
- for await (const record of tables.Product.search({
91
- conditions: [{ attribute: 'price', comparator: 'less_than', value: 100 }],
92
- limit: 20,
93
- })) { ... }
94
- ```
63
+ 4. **Query with `search(` and `conditions`**: Pass a query object to `search()` to filter records. Iterate the async result.
95
64
 
96
- **AND + nested OR:**
65
+ ```javascript
66
+ const query = {
67
+ conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
68
+ };
69
+ for await (const record of Product.search(query)) {
70
+ // ...
71
+ }
72
+ ```
97
73
 
98
- ```javascript
99
- for await (const record of tables.Product.search({
100
- conditions: [
101
- { attribute: 'price', comparator: 'less_than', value: 100 },
102
- {
103
- operator: 'or',
104
- conditions: [
105
- { attribute: 'rating', comparator: 'greater_than', value: 4 },
106
- { attribute: 'featured', value: true },
107
- ],
108
- },
109
- ],
110
- })) { ... }
111
- ```
74
+ 5. **Use `select` to shape results**: Pass a `select` array to return only specific properties, including nested relationship fields.
112
75
 
113
- **Relationship traversal:**
76
+ ```javascript
77
+ const book = await Book.get({ id: 42, select: ['id', 'title', 'author'] });
78
+ book.author.name; // full related Author record
114
79
 
115
- ```javascript
116
- for await (const record of tables.Book.search({
117
- conditions: [{ attribute: ['brand', 'name'], comparator: 'equals', value: 'Harper' }],
118
- })) { ... }
119
- ```
80
+ // Partial related record
81
+ const book = await Book.get({
82
+ id: 42,
83
+ select: ['id', 'title', { name: 'author', select: ['name'] }],
84
+ });
85
+ ```
120
86
 
121
- **Sort and paginate:**
87
+ 6. **Use `addTo` for concurrent-safe increments**: Call `addTo` on a mutable resource instance obtained via `update()`. This uses CRDT incrementation, safe across threads and nodes.
122
88
 
123
- ```javascript
124
- for await (const record of tables.Product.search({
125
- conditions: [{ attribute: 'inStock', value: true }],
126
- sort: { attribute: 'price', descending: false },
127
- limit: 10,
128
- offset: 20,
129
- })) { ... }
130
- ```
89
+ ```javascript
90
+ static async post(target, data) {
91
+ const record = await this.update(target.id);
92
+ record.addTo('quantity', -1); // decrement safely across nodes
93
+ }
94
+ ```
131
95
 
132
- ## Selecting Related Data
96
+ 7. **Scope destructive operations carefully**: `update`, `patch`, and `delete` operate directly on stored data. Always use specific `conditions`, validate the affected set before writing, and gate behind authorization controls.
133
97
 
134
- When a field is defined as a relationship (via `@relationship` — see [Defining Relationships](defining-relationships.md)), `select` can pull the related record(s) into your results as nested properties. This is the programmatic equivalent of the REST `select(name,author{name})` syntax (see [Querying REST APIs](querying-rest-apis.md)).
98
+ ## Examples
135
99
 
136
- **Whole related record** — list the relationship field by name. The related record (or an array of records for a to-many relationship) is attached as a nested property:
100
+ ### Nested conditions query
137
101
 
138
102
  ```javascript
139
- for await (const book of tables.Book.search({
140
- conditions: [{ attribute: 'id', value: 42 }],
141
- select: ['id', 'title', 'author'], // `author` is a relationship field
142
- })) {
143
- console.log(book.author.name); // the full related Author record
144
- }
103
+ Product.search({
104
+ conditions: [
105
+ { attribute: 'price', comparator: 'less_than', value: 100 },
106
+ {
107
+ operator: 'or',
108
+ conditions: [
109
+ { attribute: 'rating', comparator: 'greater_than', value: 4 },
110
+ { attribute: 'featured', value: true },
111
+ ],
112
+ },
113
+ ],
114
+ });
145
115
  ```
146
116
 
147
- **Partial related record** use an object `{ name, select }` to choose which fields of the related record to return. Unselected fields are omitted:
117
+ ### Chained attribute reference (relationship join)
148
118
 
149
119
  ```javascript
150
- for await (const book of tables.Book.search({
151
- conditions: [{ attribute: ['author', 'name'], comparator: 'equals', value: 'Harper' }],
152
- select: ['id', 'title', { name: 'author', select: ['name'] }],
153
- })) {
154
- // book.author.name is present; other Author fields are undefined
155
- }
120
+ Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
156
121
  ```
157
122
 
158
- **Nesting** a `select` inside an object entry may itself contain more `{ name, select }` objects, traversing multiple relationships in one query:
123
+ ### Deep nested `select`
159
124
 
160
125
  ```javascript
161
126
  select: [
162
- 'id',
163
- 'name',
164
- {
165
- name: 'segments',
166
- select: ['id', 'name', { name: 'client', select: ['id', 'name'] }],
167
- },
168
- ],
127
+ 'id',
128
+ 'name',
129
+ { name: 'segments', select: ['id', 'name', { name: 'client', select: ['id', 'name'] }] },
130
+ ];
169
131
  ```
170
132
 
171
- Notes:
133
+ ### SSR usage
172
134
 
173
- - A to-many relationship resolves to an array of records; depending on access pattern you may need to `await` the property before iterating it.
174
- - Selecting a relationship without filtering on it produces LEFT JOIN behavior (records with no related row are still returned); adding a condition on a related attribute (e.g. `attribute: ['author', 'name']`) produces INNER JOIN behavior.
135
+ ```typescript
136
+ import { tables } from 'harper';
175
137
 
176
- ## Cautions
138
+ export async function render(url: string): Promise<string> {
139
+ const product = await tables.Product.get(idFromUrl(url));
140
+ return renderToString(/* <App product={product} /> */);
141
+ }
142
+ ```
177
143
 
178
- Be very careful when performing updates and deletions! You may be dealing with live production data. The wrong request to delete, without approval from a human, could be devastating to a business. Always use the proper approval process.
144
+ ## Notes
145
+
146
+ ### `conditions` comparator values
147
+
148
+ | Comparator | Description |
149
+ | -------------------- | ---------------------- |
150
+ | `equals` | Default equality match |
151
+ | `greater_than` | Strictly greater |
152
+ | `greater_than_equal` | Greater than or equal |
153
+ | `less_than` | Strictly less |
154
+ | `less_than_equal` | Less than or equal |
155
+ | `starts_with` | String prefix match |
156
+ | `contains` | String contains |
157
+ | `ends_with` | String suffix match |
158
+ | `between` | Range match |
159
+ | `not_equal` | Inequality match |
160
+
161
+ ### Query object options
162
+
163
+ | Property | Description |
164
+ | ----------------------- | ----------------------------------------------------------------------- |
165
+ | `conditions` | Array of condition objects to filter records |
166
+ | `operator` | Top-level `and` (default) or `or` for the `conditions` array |
167
+ | `limit` | Maximum number of records to return |
168
+ | `offset` | Number of records to skip (for pagination) |
169
+ | `select` | Properties to include in each returned record |
170
+ | `sort` | Sort order object with `attribute`, `descending`, and `next` properties |
171
+ | `explain` | If `true`, returns conditions reordered as Harper will execute them |
172
+ | `enforceExecutionOrder` | If `true`, forces conditions to execute in the order supplied |
173
+
174
+ ### `select` special properties
175
+
176
+ - `$id` — Returns the primary key regardless of its name
177
+ - `$updatedtime` — Returns the last-updated timestamp
178
+ - `$distance` — Returns the computed distance from the target vector when the query uses a vector index
179
+
180
+ ### Relationship join behavior
181
+
182
+ - Selecting a relationship **without** filtering on it behaves as a **LEFT JOIN** — records with no related row are still returned.
183
+ - Adding a `conditions` entry on a related attribute (e.g. `attribute: ['author', 'name']`) behaves as an **INNER JOIN** — only records with a matching related row are returned.
184
+
185
+ - Keep `harper` external when bundling for SSR (e.g. `ssr: { external: ['harper'] }` in `vite.config`) so it resolves to the runtime instead of being bundled.
186
+ - `tables`, `databases`, and other Harper APIs are the same live, process-wide objects regardless of whether accessed as globals or via `import { tables } from 'harper'`.
@@ -68,6 +68,34 @@ static:
68
68
  - Install dependencies: `npm install --save-dev vite @harperfast/vite @vitejs/plugin-react` (swap in your framework's Vite plugin, e.g. `@vitejs/plugin-vue`).
69
69
  - Then `harper dev .` runs the app with HMR and `harper run .` runs the production build. Vite does _not_ need to be executed separately.
70
70
 
71
+ ## Reading Harper Data During SSR
72
+
73
+ The render entry (`src/entry-server.tsx`) runs **inside Harper**, so it can read straight from the database and render the data into the HTML — no client-side fetch/XHR. `tables` is the same live, process-wide registry available everywhere (see [Programmatic Table Requests](programmatic-table-requests.md)); import it and query a table in an async `render`:
74
+
75
+ ```tsx
76
+ import { tables } from 'harper';
77
+
78
+ export async function render(url: string): Promise<string> {
79
+ const product = await tables.Product.get(idFromUrl(url));
80
+ return renderToString(
81
+ <StrictMode>
82
+ <App product={product} />
83
+ </StrictMode>,
84
+ );
85
+ }
86
+ ```
87
+
88
+ Keep `harper` external in `vite.config.ts` so this import resolves to Harper's running runtime instead of being bundled. `node_modules/harper` is symlinked to the running install, and symlinked deps aren't reliably auto-externalized for SSR:
89
+
90
+ ```typescript
91
+ export default defineConfig({
92
+ ssr: { external: ['harper'] },
93
+ // ...plugins, resolve, build
94
+ });
95
+ ```
96
+
97
+ To hydrate on the client without re-fetching, embed the rendered data in the HTML (e.g. an inline `<script type="application/json">`) and read it back before hydration — so the page needs no XHR at all.
98
+
71
99
  ## Deploying to Production
72
100
 
73
101
  Because `@harperfast/vite` builds on the node and `static` serves the output, deploy the component as-is — no manual build-and-move step is needed:
@@ -8,21 +8,21 @@ metadata:
8
8
  - reference/v5/database/api.md#Streaming
9
9
  - reference/v5/database/api.md#`BlobOptions`
10
10
  - reference/v5/database/api.md#Blob Coercion
11
- sourceCommit: 4fe4c9c95e0974eaa77032f6f10e36fbd8ec64ac
12
- inputHash: 71a3e738ebf87fa4
11
+ sourceCommit: f37a8c4021e20d5c74c1d339a6b6c8c196b5603e
12
+ inputHash: 92e03eb0b830f335
13
13
  ---
14
14
 
15
15
  # Using the Blob Data Type
16
16
 
17
- Instructions for the agent to follow when storing and retrieving large binary content using Harper's `Blob` data type.
17
+ Instructions for the agent to follow when storing and retrieving large binary content using the `Blob` data type in Harper.
18
18
 
19
19
  ## When to Use
20
20
 
21
- Apply this rule when a schema field needs to store large binary content such as images, video, audio, or large HTML — typically content larger than 20KB. Use `Blob` instead of `Bytes` when you need streaming support or want to avoid loading the entire value into memory. See [handling-binary-data.md](handling-binary-data.md) for broader binary data guidance.
21
+ Apply this rule when a schema field needs to store large binary content such as images, video, audio, or large HTML — typically content larger than 20KB. Use `Blob` instead of `Bytes` when streaming support and out-of-record storage are required. See [handling-binary-data.md](handling-binary-data.md) for broader binary data guidance.
22
22
 
23
23
  ## How It Works
24
24
 
25
- 1. **Declare a `Blob` field in your schema**: Add a field typed as `Blob` to a `@table` type.
25
+ 1. **Declare a `Blob` field in your schema**: Add a field typed as `Blob` to your `@table` type.
26
26
 
27
27
  ```graphql
28
28
  type MyTable @table {
@@ -31,14 +31,14 @@ Apply this rule when a schema field needs to store large binary content such as
31
31
  }
32
32
  ```
33
33
 
34
- 2. **Create a blob with `createBlob()`**: Pass a buffer, string, or stream as the first argument. Pass a `BlobOptions` object as the second argument to configure behavior.
34
+ 2. **Create and store a blob with `createBlob()`**: Pass a buffer or stream to `createBlob()`, then `put` the record.
35
35
 
36
36
  ```javascript
37
37
  let blob = createBlob(largeBuffer);
38
38
  await MyTable.put({ id: 'my-record', data: blob });
39
39
  ```
40
40
 
41
- 3. **Read blob data using standard Web API methods**: The `Blob` type implements the Web API `Blob` interface. Use `.bytes()`, `.text()`, `.arrayBuffer()`, `.stream()`, or `.slice()` to access content.
41
+ 3. **Retrieve blob data using standard Web API methods**: The `Blob` type implements the Web API `Blob` interface. Use `.bytes()`, `.text()`, `.arrayBuffer()`, `.stream()`, or `.slice()` as needed.
42
42
 
43
43
  ```javascript
44
44
  let record = await MyTable.get('my-record');
@@ -47,7 +47,7 @@ Apply this rule when a schema field needs to store large binary content such as
47
47
  let stream = record.data.stream(); // ReadableStream
48
48
  ```
49
49
 
50
- 4. **Use `saveBeforeCommit` for ACID-compliant writes**: By default, blobs are not ACID-compliant — a record can reference a blob before it is fully written. Set `saveBeforeCommit: true` to wait for the full write before the transaction commits.
50
+ 4. **Use `saveBeforeCommit` when full write must precede commit**: By default, `Blob` is not ACID-compliant — a record can reference a blob before it is fully written. Set `saveBeforeCommit: true` to block the transaction until the blob is fully saved.
51
51
 
52
52
  ```javascript
53
53
  let blob = createBlob(stream, { saveBeforeCommit: true });
@@ -55,7 +55,7 @@ Apply this rule when a schema field needs to store large binary content such as
55
55
  // put() resolves only after blob is fully written and record is committed
56
56
  ```
57
57
 
58
- 5. **Register an error handler when returning a blob via REST**: Interrupted streams must be handled explicitly to avoid stale records.
58
+ 5. **Register an error handler when returning a blob via REST**: Interrupted streams must be handled explicitly.
59
59
 
60
60
  ```javascript
61
61
  export class MyEndpoint extends MyTable {
@@ -70,15 +70,17 @@ Apply this rule when a schema field needs to store large binary content such as
70
70
  }
71
71
  ```
72
72
 
73
- 6. **Rely on automatic coercion where applicable**: When a field is typed as `Blob` in the schema, any string or buffer assigned via `put`, `patch`, or `publish` is automatically coerced to a `Blob`. Manual `createBlob()` calls are not required for plain JSON HTTP bodies or MQTT messages in most cases.
73
+ 6. **Rely on automatic coercion where applicable**: When a field is typed as `Blob` in the schema, any string or buffer assigned via `put`, `patch`, or `publish` is automatically coerced to a `Blob` no manual `createBlob()` call is needed in those cases.
74
74
 
75
- ### `BlobOptions` Reference
75
+ ### `BlobOptions` reference
76
+
77
+ Pass an options object as the second argument to `createBlob()`.
76
78
 
77
79
  | Option | Type | Default | Description |
78
80
  | ------------------ | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
79
81
  | `type` | `string` | `undefined` | MIME type to associate with the blob (e.g., `image/jpeg`). Readable via `blob.type` and used when serving HTTP. |
80
82
  | `size` | `number` | `undefined` | Size of the data in bytes, if known ahead of time. Otherwise inferred from a buffer or determined as a stream completes. |
81
- | `saveBeforeCommit` | `boolean` | `false` | Wait for the blob to be fully written before committing the transaction. |
83
+ | `saveBeforeCommit` | `boolean` | `false` | Wait until the blob is fully written before the transaction commits. |
82
84
  | `compress` | `boolean` | `false` | Compress the stored data with deflate. |
83
85
  | `flush` | `boolean` | `false` | Flush the file to disk after writing, before the `createBlob` promise chain resolves. |
84
86
 
@@ -91,7 +93,7 @@ let blob = createBlob(imageBuffer, { type: 'image/jpeg' });
91
93
  await Photo.put({ id, data: blob });
92
94
  ```
93
95
 
94
- **Stream large media with low latency:**
96
+ **Stream a blob in as it streams out (low-latency passthrough):**
95
97
 
96
98
  ```javascript
97
99
  let blob = createBlob(incomingStream);
@@ -103,7 +105,7 @@ let record = await MyTable.get('my-record');
103
105
  let outgoingStream = record.data.stream();
104
106
  ```
105
107
 
106
- **Guaranteed write before commit:**
108
+ **Guarantee full write before commit using `saveBeforeCommit`:**
107
109
 
108
110
  ```javascript
109
111
  let blob = createBlob(stream, { saveBeforeCommit: true });
@@ -112,7 +114,6 @@ await MyTable.put({ id: 'my-record', data: blob });
112
114
 
113
115
  ## Notes
114
116
 
115
- - `Blob` stores data separately from the record; `Bytes` does not. Prefer `Blob` for content larger than 20KB.
116
- - All standard Web API `Blob` methods are available: `.bytes()`, `.text()`, `.arrayBuffer()`, `.stream()`, `.slice()`.
117
- - Blobs are **not** ACID-compliant by default when created from a stream. Use `saveBeforeCommit: true` to enforce transactional consistency.
118
- - Always attach an `error` handler on blobs returned as HTTP response bodies to handle interrupted streams.
117
+ - `Blob` stores data separately from the record. If you need the binary data to be a true, ACID-committed part of the record, use a `Bytes` field instead.
118
+ - All standard Web API `Blob` methods `.text()`, `.arrayBuffer()`, `.stream()`, `.slice()`, and `.bytes()` — are available on retrieved blob fields.
119
+ - Without `saveBeforeCommit: true`, blobs are **not** ACID-compliant by default; a record can reference a blob before it is fully written to storage.