@harperfast/template-vue-ts-studio 1.9.2 → 1.10.1
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 +916 -338
- package/.agents/skills/harper-best-practices/rules/caching.md +68 -62
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +144 -23
- package/.agents/skills/harper-best-practices/rules/defining-relationships.md +152 -22
- package/.agents/skills/harper-best-practices/rules/extending-tables.md +90 -21
- package/.agents/skills/harper-best-practices/rules/handling-binary-data.md +103 -23
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +143 -91
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
- package/.agents/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +97 -16
- package/.agents/skills/harper-best-practices/rules/vector-indexing.md +59 -29
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +109 -7
- package/agent/skills/harper-best-practices/AGENTS.md +916 -338
- package/agent/skills/harper-best-practices/rules/caching.md +68 -62
- package/agent/skills/harper-best-practices/rules/custom-resources.md +144 -23
- package/agent/skills/harper-best-practices/rules/defining-relationships.md +152 -22
- package/agent/skills/harper-best-practices/rules/extending-tables.md +90 -21
- package/agent/skills/harper-best-practices/rules/handling-binary-data.md +103 -23
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +143 -91
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
- package/agent/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +97 -16
- package/agent/skills/harper-best-practices/rules/vector-indexing.md +59 -29
- package/agent/skills/harper-best-practices/rules.manifest.yaml +109 -7
- package/package.json +1 -1
- package/skills-lock.json +1 -1
|
@@ -2,45 +2,114 @@
|
|
|
2
2
|
name: extending-tables
|
|
3
3
|
description: How to add custom logic to automatically generated table resources in Harper.
|
|
4
4
|
metadata:
|
|
5
|
-
mode:
|
|
5
|
+
mode: generate
|
|
6
|
+
sources:
|
|
7
|
+
- reference/v5/resources/overview.md#Extending a Table
|
|
8
|
+
- reference/v5/resources/resource-api.md#Throwing Errors
|
|
9
|
+
sourceCommit: ce0ab713d918d789bc1c9f22e461e963ccc1dff1
|
|
10
|
+
inputHash: 19738fbc732e0a1a
|
|
6
11
|
---
|
|
7
12
|
|
|
8
13
|
# Extending Tables
|
|
9
14
|
|
|
10
|
-
Instructions for the agent to follow when
|
|
15
|
+
Instructions for the agent to follow when adding custom logic to automatically generated table resources in Harper.
|
|
11
16
|
|
|
12
17
|
## When to Use
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
Apply this rule when you need to add computed properties, intercept writes, enforce validation, or otherwise customize the behavior of a Harper table resource beyond what the default generated endpoints provide. Use it any time a `@table` type needs server-side logic attached to its REST handlers.
|
|
15
20
|
|
|
16
21
|
## How It Works
|
|
17
22
|
|
|
18
|
-
1. **Define the
|
|
23
|
+
1. **Define the schema without `@export`**: Declare the table type in `schema.graphql` and omit the `@export` directive. Leaving `@export` on the schema while also exporting a subclass with the same name produces conflicting endpoints. Let the JavaScript class own the URL instead.
|
|
24
|
+
|
|
19
25
|
```graphql
|
|
26
|
+
# Omit the `@export` directive
|
|
20
27
|
type MyTable @table {
|
|
21
|
-
id:
|
|
22
|
-
|
|
28
|
+
id: Long @primaryKey
|
|
29
|
+
# ...
|
|
23
30
|
}
|
|
24
31
|
```
|
|
25
|
-
2. **Create the Extension File**: Create a `.ts` file in your `resources/` directory.
|
|
26
|
-
3. **Extend the Table Resource**: Export a class that extends `tables.YourTableName` and override the relevant **static** methods. In Harper 5 resource handlers are static and map 1:1 to HTTP verbs: `get(target)`, `post(target, data)`, `put(target, data)`, `patch(target, data)`, `delete(target)`. `target` is a pre-parsed `RequestTarget`; for writes, `data` is the request body and is **awaitable** (`await data`). Delegate to `super` to keep Harper's default behavior — a collection create passes just the record (`super.post(record)`), updates pass the target (`super.put(target, data)` / `super.patch(target, data)`), and reads/deletes pass the target (`super.get(target)`). To return a specific HTTP status from a thrown error, set **`.statusCode`** (e.g. `400`) on the error — a plain `.status` property is ignored.
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
import { tables } from 'harper';
|
|
33
|
+
2. **Extend the generated table class**: In `resources.js`, extend from the `tables.<TypeName>` global. The class name you export becomes the URL path. The exported class extends tables.
|
|
30
34
|
|
|
35
|
+
```javascript
|
|
31
36
|
export class MyTable extends tables.MyTable {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return super.post(record); // create delegates with the record (no id)
|
|
37
|
+
static async get(target) {
|
|
38
|
+
const record = await super.get(target);
|
|
39
|
+
return { ...record, computedField: 'value' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static async post(target, data) {
|
|
43
|
+
this.create({ ...(await data), status: 'pending' });
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
```
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
3. **Call `super` to preserve default behavior**: When delegating to `super`, match the argument form to the operation:
|
|
49
|
+
- Reads/deletes: `super.get(target)` / `super.delete(target)`
|
|
50
|
+
- Collection create: `super.post(target, record)` — target carries no id
|
|
51
|
+
- Updates: `super.put(target, data)` / `super.patch(target, data)`
|
|
52
|
+
|
|
53
|
+
Omit the `super` call only if you intend to replace the default behavior entirely.
|
|
54
|
+
|
|
55
|
+
4. **Set `statusCode` on thrown errors to control HTTP responses**: Uncaught errors are caught by the protocol handler and produce error responses for REST. Use `.statusCode` — a plain `.status` property is ignored.
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const error = new Error('Name is required');
|
|
59
|
+
error.statusCode = 400; // use statusCode, NOT status
|
|
60
|
+
throw error;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
5. **Configure Harper to load both files**: Ensure your configuration references the schema and resource files.
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
rest: true
|
|
67
|
+
graphqlSchema:
|
|
68
|
+
files: schema.graphql
|
|
69
|
+
jsResource:
|
|
70
|
+
files: resources.js
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
Full end-to-end example — schema, resource class, and error handling:
|
|
76
|
+
|
|
77
|
+
```graphql
|
|
78
|
+
# schema.graphql — omit @export so the JS class owns the endpoint
|
|
79
|
+
type MyTable @table {
|
|
80
|
+
id: Long @primaryKey
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// resources.js
|
|
86
|
+
export class MyTable extends tables.MyTable {
|
|
87
|
+
static async get(target) {
|
|
88
|
+
// get the record from the database
|
|
89
|
+
const record = await super.get(target);
|
|
90
|
+
// add a computed property before returning
|
|
91
|
+
return { ...record, computedField: 'value' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static async post(target, data) {
|
|
95
|
+
// custom action on POST
|
|
96
|
+
this.create({ ...(await data), status: 'pending' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Throwing a controlled HTTP error:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
if (!authorized) {
|
|
105
|
+
const error = new Error('Forbidden');
|
|
106
|
+
error.statusCode = 403;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Notes
|
|
112
|
+
|
|
113
|
+
- Always omit `@export` from the schema type when a JavaScript subclass is exporting the same name. The two registrations conflict.
|
|
114
|
+
- `super` must be called with the correct arguments for each operation type — mismatched arguments will not behave as expected.
|
|
115
|
+
- `statusCode` is the only recognized property for controlling HTTP status on thrown errors; `.status` is ignored.
|
|
@@ -2,44 +2,124 @@
|
|
|
2
2
|
name: handling-binary-data
|
|
3
3
|
description: How to store and serve binary data like images or audio in Harper.
|
|
4
4
|
metadata:
|
|
5
|
-
mode:
|
|
5
|
+
mode: generate
|
|
6
|
+
sources:
|
|
7
|
+
- reference/v5/database/api.md#Accepting Binary in JSON Requests
|
|
8
|
+
- reference/v5/database/api.md#Serving Binary from a Resource
|
|
9
|
+
- reference/v5/rest/content-types.md#Storing Arbitrary Content Types
|
|
10
|
+
sourceCommit: ce0ab713d918d789bc1c9f22e461e963ccc1dff1
|
|
11
|
+
inputHash: fa06480e6fae7614
|
|
6
12
|
---
|
|
7
13
|
|
|
8
14
|
# Handling Binary Data
|
|
9
15
|
|
|
10
|
-
Instructions for the agent to follow when
|
|
16
|
+
Instructions for the agent to follow when storing and serving binary data (images, audio, arbitrary content types) in Harper.
|
|
11
17
|
|
|
12
18
|
## When to Use
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
Apply this rule when a Harper resource needs to accept, store, or serve binary payloads such as images, audio files, or calendar data. Use it when REST clients send `base64`-encoded data inside JSON, when raw binary is uploaded via `PUT`/`POST`, or when a resource must stream binary back to the client with the correct `Content-Type`.
|
|
15
21
|
|
|
16
22
|
## How It Works
|
|
17
23
|
|
|
18
|
-
1. **
|
|
24
|
+
1. **Accept base64-encoded binary from JSON clients**: Decode the incoming `base64` string with `Buffer.from` and wrap it using `createBlob`, recording the MIME type. Override `post` in your resource class:
|
|
19
25
|
|
|
20
26
|
```typescript
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
import { type RequestTargetOrId, tables, createBlob } from 'harper';
|
|
28
|
+
|
|
29
|
+
export class Photo extends tables.Photo {
|
|
30
|
+
static async post(target: RequestTargetOrId, record: any) {
|
|
31
|
+
if (record.data) {
|
|
32
|
+
record.data = createBlob(Buffer.from(record.data, record.encoding || 'base64'), {
|
|
33
|
+
type: record.contentType || 'application/octet-stream',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return super.post(target, record);
|
|
37
|
+
}
|
|
28
38
|
}
|
|
29
39
|
```
|
|
30
40
|
|
|
31
|
-
2. **Serve
|
|
41
|
+
2. **Serve binary from a resource**: Override `get` to return a response object with the blob's MIME type in the `Content-Type` header and the blob as the body. Harper streams it to the client:
|
|
42
|
+
|
|
32
43
|
```typescript
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
export class Photo extends tables.Photo {
|
|
45
|
+
static async get(target: RequestTargetOrId) {
|
|
46
|
+
const record = await super.get(target);
|
|
47
|
+
if (record?.data) {
|
|
48
|
+
return {
|
|
49
|
+
status: 200,
|
|
50
|
+
headers: { 'Content-Type': record.data.type || 'application/octet-stream' },
|
|
51
|
+
body: record.data,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return record;
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
57
|
```
|
|
45
|
-
|
|
58
|
+
|
|
59
|
+
3. **Upload raw binary with a non-standard content type**: Make a `PUT` or `POST` with any non-standard `Content-Type` header. Harper automatically stores the body as a record with `contentType` and `data` properties:
|
|
60
|
+
|
|
61
|
+
```http
|
|
62
|
+
PUT /my-resource/33
|
|
63
|
+
Content-Type: text/calendar
|
|
64
|
+
|
|
65
|
+
BEGIN:VCALENDAR
|
|
66
|
+
VERSION:2.0
|
|
67
|
+
...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Harper stores this as:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{ "contentType": "text/calendar", "data": "BEGIN:VCALENDAR\nVERSION:2.0\n..." }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Retrieving that record returns the response with the stored `Content-Type` and body. If the content type is not from the `text` family, the data is treated as binary (a Node.js `Buffer`).
|
|
77
|
+
|
|
78
|
+
4. **Upload binary to a specific property**: Use `application/octet-stream` (or any image/binary MIME type) and target a sub-path to store binary directly on a property:
|
|
79
|
+
|
|
80
|
+
```http
|
|
81
|
+
PUT /my-resource/33/image
|
|
82
|
+
Content-Type: image/gif
|
|
83
|
+
|
|
84
|
+
...image data...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
**End-to-end: accept base64 JSON, store as blob, serve as binary**
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { type RequestTargetOrId, tables, createBlob } from 'harper';
|
|
93
|
+
|
|
94
|
+
export class Photo extends tables.Photo {
|
|
95
|
+
// Accept base64-encoded uploads in JSON
|
|
96
|
+
static async post(target: RequestTargetOrId, record: any) {
|
|
97
|
+
if (record.data) {
|
|
98
|
+
record.data = createBlob(Buffer.from(record.data, record.encoding || 'base64'), {
|
|
99
|
+
type: record.contentType || 'application/octet-stream',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return super.post(target, record);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Stream the blob back with the correct Content-Type
|
|
106
|
+
static async get(target: RequestTargetOrId) {
|
|
107
|
+
const record = await super.get(target);
|
|
108
|
+
if (record?.data) {
|
|
109
|
+
return {
|
|
110
|
+
status: 200,
|
|
111
|
+
headers: { 'Content-Type': record.data.type || 'application/octet-stream' },
|
|
112
|
+
body: record.data,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return record;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Notes
|
|
121
|
+
|
|
122
|
+
- `createBlob` takes a `Buffer` as its first argument and an options object with a `type` property for the MIME type. See [using-blob-datatype.md](using-blob-datatype.md) for full details on the blob data type.
|
|
123
|
+
- Always fall back to `application/octet-stream` when no MIME type is known, both when creating and when serving blobs.
|
|
124
|
+
- When Harper retrieves a record that has both `contentType` and `data` properties, it automatically sets the response `Content-Type` and body — no custom `get` override is required for that case unless you need additional logic.
|
|
125
|
+
- Non-`text` content types cause `data` to be stored and returned as a Node.js `Buffer`.
|
|
@@ -2,133 +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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
}
|
|
31
|
+
1. **Import `tables`**: Import from the `harper` package. Each table defined in `schema.graphql` with `@table` is available as a named property.
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { tables } from 'harper';
|
|
35
|
+
const { Product } = tables;
|
|
36
|
+
// same as: databases.data.Product
|
|
34
37
|
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
2. **Define your schema**: Declare tables in `schema.graphql` using `@table` and `@primaryKey`.
|
|
40
|
+
|
|
41
|
+
```graphql
|
|
42
|
+
# schema.graphql
|
|
43
|
+
type Product @table {
|
|
44
|
+
id: Long @primaryKey
|
|
45
|
+
name: String
|
|
46
|
+
price: Float
|
|
40
47
|
}
|
|
41
48
|
```
|
|
42
|
-
6. **Publish Events**: Use `publish(id, message)` to trigger subscriptions without necessarily persisting data.
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
3. **Create and mutate records**: Use `create`, `patch`, `get`, and `update` on the table class.
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
```javascript
|
|
53
|
+
// Create a new record (id auto-generated)
|
|
54
|
+
const created = await Product.create({ name: 'Shirt', price: 9.5 });
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
// Modify the record
|
|
57
|
+
await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 |
|
|
59
|
+
// Retrieve by primary key
|
|
60
|
+
const record = await Product.get(created.id);
|
|
61
|
+
```
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
4. **Query with `search(` and `conditions`**: Pass a query object to `search()` to filter records. Iterate the async result.
|
|
59
64
|
|
|
60
|
-
|
|
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
|
+
```
|
|
61
73
|
|
|
62
|
-
|
|
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]`) |
|
|
74
|
+
5. **Use `select` to shape results**: Pass a `select` array to return only specific properties, including nested relationship fields.
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
```javascript
|
|
77
|
+
const book = await Book.get({ id: 42, select: ['id', 'title', 'author'] });
|
|
78
|
+
book.author.name; // full related Author record
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
| `sort` | Object with `attribute`, `descending` (bool), and optional `next` for secondary sort |
|
|
80
|
+
// Partial related record
|
|
81
|
+
const book = await Book.get({
|
|
82
|
+
id: 42,
|
|
83
|
+
select: ['id', 'title', { name: 'author', select: ['name'] }],
|
|
84
|
+
});
|
|
85
|
+
```
|
|
84
86
|
|
|
85
|
-
|
|
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.
|
|
86
88
|
|
|
87
|
-
|
|
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
|
+
```
|
|
88
95
|
|
|
89
|
-
|
|
90
|
-
for await (const record of tables.Product.search({
|
|
91
|
-
conditions: [{ attribute: 'price', comparator: 'less_than', value: 100 }],
|
|
92
|
-
limit: 20,
|
|
93
|
-
})) { ... }
|
|
94
|
-
```
|
|
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.
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
## Examples
|
|
99
|
+
|
|
100
|
+
### Nested conditions query
|
|
97
101
|
|
|
98
102
|
```javascript
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
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
|
+
});
|
|
111
115
|
```
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
### Chained attribute reference (relationship join)
|
|
114
118
|
|
|
115
119
|
```javascript
|
|
116
|
-
|
|
117
|
-
conditions: [{ attribute: ['brand', 'name'], comparator: 'equals', value: 'Harper' }],
|
|
118
|
-
})) { ... }
|
|
120
|
+
Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
|
|
119
121
|
```
|
|
120
122
|
|
|
121
|
-
|
|
123
|
+
### Deep nested `select`
|
|
122
124
|
|
|
123
125
|
```javascript
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})) { ... }
|
|
126
|
+
select: [
|
|
127
|
+
'id',
|
|
128
|
+
'name',
|
|
129
|
+
{ name: 'segments', select: ['id', 'name', { name: 'client', select: ['id', 'name'] }] },
|
|
130
|
+
];
|
|
130
131
|
```
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
### SSR usage
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { tables } from 'harper';
|
|
137
|
+
|
|
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
|
+
```
|
|
133
143
|
|
|
134
|
-
|
|
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'`.
|