@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.
- package/.agents/skills/harper-best-practices/AGENTS.md +228 -166
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +65 -27
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +139 -131
- package/.agents/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +19 -18
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +26 -1
- package/agent/skills/harper-best-practices/AGENTS.md +228 -166
- package/agent/skills/harper-best-practices/rules/custom-resources.md +65 -27
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +139 -131
- package/agent/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +19 -18
- package/agent/skills/harper-best-practices/rules.manifest.yaml +26 -1
- package/package.json +1 -1
- package/skills-lock.json +1 -1
|
@@ -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:
|
|
11
|
-
inputHash:
|
|
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
|
|
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
|
|
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`**:
|
|
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. **
|
|
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. **
|
|
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
|
|
63
|
-
|
|
|
64
|
-
| `export class Foo extends Resource {}`
|
|
65
|
-
| `export const Bar = { Foo };`
|
|
66
|
-
| `export const bar = { 'foo-baz': Foo };`
|
|
67
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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',
|
|
155
|
+
server.resources.set('my-path', Foo);
|
|
119
156
|
```
|
|
120
157
|
|
|
121
158
|
## Notes
|
|
122
159
|
|
|
123
|
-
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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. **
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
39
|
+
2. **Define your schema**: Declare tables in `schema.graphql` using `@table` and `@primaryKey`.
|
|
61
40
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
50
|
+
3. **Create and mutate records**: Use `create`, `patch`, `get`, and `update` on the table class.
|
|
76
51
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
56
|
+
// Modify the record
|
|
57
|
+
await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
|
|
86
58
|
|
|
87
|
-
|
|
59
|
+
// Retrieve by primary key
|
|
60
|
+
const record = await Product.get(created.id);
|
|
61
|
+
```
|
|
88
62
|
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
## Examples
|
|
135
99
|
|
|
136
|
-
|
|
100
|
+
### Nested conditions query
|
|
137
101
|
|
|
138
102
|
```javascript
|
|
139
|
-
|
|
140
|
-
conditions: [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
117
|
+
### Chained attribute reference (relationship join)
|
|
148
118
|
|
|
149
119
|
```javascript
|
|
150
|
-
|
|
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
|
-
|
|
123
|
+
### Deep nested `select`
|
|
159
124
|
|
|
160
125
|
```javascript
|
|
161
126
|
select: [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
133
|
+
### SSR usage
|
|
172
134
|
|
|
173
|
-
|
|
174
|
-
|
|
135
|
+
```typescript
|
|
136
|
+
import { tables } from 'harper';
|
|
175
137
|
|
|
176
|
-
|
|
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
|
-
|
|
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:
|
|
12
|
-
inputHash:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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. **
|
|
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`
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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
|
|
116
|
-
- All standard Web API `Blob` methods
|
|
117
|
-
-
|
|
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.
|