@harperfast/template-vue-ts-studio 1.9.1 → 1.10.0
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 +770 -254
- package/.agents/skills/harper-best-practices/rules/caching.md +68 -62
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +106 -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 +51 -7
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +96 -16
- package/.agents/skills/harper-best-practices/rules/vector-indexing.md +59 -29
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +83 -6
- package/agent/skills/harper-best-practices/AGENTS.md +770 -254
- package/agent/skills/harper-best-practices/rules/caching.md +68 -62
- package/agent/skills/harper-best-practices/rules/custom-resources.md +106 -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 +51 -7
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +96 -16
- package/agent/skills/harper-best-practices/rules/vector-indexing.md +59 -29
- package/agent/skills/harper-best-practices/rules.manifest.yaml +83 -6
- 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`.
|
|
@@ -74,13 +74,13 @@ Use these exact strings — incorrect comparator names will silently fail or err
|
|
|
74
74
|
|
|
75
75
|
### Query Object Parameters
|
|
76
76
|
|
|
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
|
|
83
|
-
| `sort` | Object with `attribute`, `descending` (bool), and optional `next` for secondary sort
|
|
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 |
|
|
84
84
|
|
|
85
85
|
### Examples
|
|
86
86
|
|
|
@@ -129,6 +129,50 @@ for await (const record of tables.Product.search({
|
|
|
129
129
|
})) { ... }
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
## Selecting Related Data
|
|
133
|
+
|
|
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)).
|
|
135
|
+
|
|
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:
|
|
137
|
+
|
|
138
|
+
```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
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Partial related record** — use an object `{ name, select }` to choose which fields of the related record to return. Unselected fields are omitted:
|
|
148
|
+
|
|
149
|
+
```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
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Nesting** — a `select` inside an object entry may itself contain more `{ name, select }` objects, traversing multiple relationships in one query:
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
select: [
|
|
162
|
+
'id',
|
|
163
|
+
'name',
|
|
164
|
+
{
|
|
165
|
+
name: 'segments',
|
|
166
|
+
select: ['id', 'name', { name: 'client', select: ['id', 'name'] }],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Notes:
|
|
172
|
+
|
|
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.
|
|
175
|
+
|
|
132
176
|
## Cautions
|
|
133
177
|
|
|
134
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.
|
|
@@ -9,11 +9,11 @@ metadata:
|
|
|
9
9
|
- reference/v5/database/schema.md#Overview
|
|
10
10
|
- reference/v5/database/schema.md#Type Directives
|
|
11
11
|
- reference/v5/database/schema.md#Field Directives
|
|
12
|
-
sourceCommit:
|
|
13
|
-
inputHash:
|
|
12
|
+
sourceCommit: 4fe4c9c95e0974eaa77032f6f10e36fbd8ec64ac
|
|
13
|
+
inputHash: 15b9c72decc1f05d
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
# Schema Design and Tooling
|
|
16
|
+
# Schema Design and GraphQL Tooling
|
|
17
17
|
|
|
18
18
|
Instructions for the agent to follow when designing Harper schemas, applying core directives, and configuring GraphQL tooling.
|
|
19
19
|
|
|
@@ -23,7 +23,18 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
|
|
|
23
23
|
|
|
24
24
|
## How It Works
|
|
25
25
|
|
|
26
|
-
1. **Create a
|
|
26
|
+
1. **Create a schema file** using standard GraphQL type definitions with Harper-specific directives. Name it (e.g., `schema.graphql`) and place it in your component directory.
|
|
27
|
+
|
|
28
|
+
2. **Register the schema** in the component's `config.yaml` using the `graphqlSchema` plugin:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
graphqlSchema:
|
|
32
|
+
files: 'schema.graphql'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Both plugins and applications can specify schemas.
|
|
36
|
+
|
|
37
|
+
3. **Mark types as tables** with `@table`. The type name becomes the table name by default:
|
|
27
38
|
|
|
28
39
|
```graphql
|
|
29
40
|
type Dog @table {
|
|
@@ -32,23 +43,36 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
|
|
|
32
43
|
breed: String
|
|
33
44
|
age: Int
|
|
34
45
|
}
|
|
46
|
+
```
|
|
35
47
|
|
|
48
|
+
4. **Designate a primary key** with `@primaryKey` on exactly one field per type. Primary keys must be unique; duplicate-key inserts are rejected. If no primary key is provided on insert, Harper auto-generates one:
|
|
49
|
+
- `String` or `ID` → UUID string
|
|
50
|
+
- `Int`, `Long`, or `Any` → auto-incrementing integer
|
|
51
|
+
|
|
52
|
+
Use `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
|
|
53
|
+
|
|
54
|
+
5. **Add secondary indexes** with `@indexed` on any field that will be used for filtering in REST queries, SQL, or NoSQL operations:
|
|
55
|
+
|
|
56
|
+
```graphql
|
|
36
57
|
type Breed @table {
|
|
37
58
|
id: Long @primaryKey
|
|
38
59
|
name: String @indexed
|
|
39
60
|
}
|
|
40
61
|
```
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
If the field value is an array, each element is individually indexed. Null values are indexed by default.
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
6. **Expose a table as an external endpoint** with `@export`. The optional `name` parameter sets the URL path segment:
|
|
66
|
+
|
|
67
|
+
```graphql
|
|
68
|
+
type MyTable @table @export(name: "my-table") {
|
|
69
|
+
id: Long @primaryKey
|
|
70
|
+
}
|
|
47
71
|
```
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
Without `name`, the type name is used as the path segment.
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
7. **Configure `@table` arguments** as needed for database placement, expiration, eviction, and replication:
|
|
52
76
|
|
|
53
77
|
| Argument | Type | Default | Description |
|
|
54
78
|
| -------------- | --------- | ----------------------------- | ------------------------------------------------------------- |
|
|
@@ -59,32 +83,13 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
|
|
|
59
83
|
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
|
|
60
84
|
| `replicate` | `Boolean` | `true` | Enable replication of this table |
|
|
61
85
|
|
|
62
|
-
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
5. **Index fields that need fast querying** with `@indexed`. This is required for filtering by that attribute in REST queries, SQL, or NoSQL operations. If the field value is an array, each element is individually indexed.
|
|
69
|
-
|
|
70
|
-
```graphql
|
|
71
|
-
type Product @table {
|
|
72
|
-
id: Long @primaryKey
|
|
73
|
-
category: String @indexed
|
|
74
|
-
price: Float @indexed
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
6. **Expose a table as an external resource endpoint** with `@export`. This makes the table accessible via REST, MQTT, and other interfaces. The optional `name` parameter sets the URL path segment; without it, the type name is used.
|
|
79
|
-
|
|
80
|
-
```graphql
|
|
81
|
-
type MyTable @table @export(name: "my-table") {
|
|
82
|
-
id: Long @primaryKey
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
7. **Restrict extra properties** with `@sealed` when records must not include attributes beyond those declared. By default, Harper allows additional properties.
|
|
86
|
+
8. **Apply additional field directives** where needed:
|
|
87
|
+
- `@createdTime` — auto-assigns Unix epoch milliseconds on record creation
|
|
88
|
+
- `@updatedTime` — auto-assigns Unix epoch milliseconds on each update
|
|
89
|
+
- `@embed(source: "fieldName", model: "modelName")` — computes an embedding vector on write; attribute type must be `[Float]`
|
|
90
|
+
- `@hidden` — suppresses the field from MCP tool descriptors and OpenAPI documents (not an access-control mechanism)
|
|
87
91
|
|
|
92
|
+
9. **Restrict extra properties** with `@sealed` if records must not include properties beyond those declared:
|
|
88
93
|
```graphql
|
|
89
94
|
type StrictRecord @table @sealed {
|
|
90
95
|
id: Long @primaryKey
|
|
@@ -92,70 +97,88 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
|
|
|
92
97
|
}
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
8. **Configure expiration, eviction, and scan behavior** together when building caching tables. These three arguments control the full record lifecycle:
|
|
96
|
-
- `expiration` — record becomes stale; next request triggers a source fetch
|
|
97
|
-
- `eviction` — additional time after `expiration` before physical removal
|
|
98
|
-
- `scanInterval` — how often Harper scans for records to evict; clock-aligned, not startup-aligned
|
|
99
|
-
|
|
100
100
|
## Examples
|
|
101
101
|
|
|
102
|
-
**
|
|
102
|
+
**Minimal two-table schema:**
|
|
103
103
|
|
|
104
104
|
```graphql
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
type Dog @table {
|
|
106
|
+
id: Long @primaryKey
|
|
107
|
+
name: String
|
|
108
|
+
breed: String
|
|
109
|
+
age: Int
|
|
109
110
|
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**Table in a named database with expiration and an indexed field:**
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
type Event @table(database: "analytics", expiration: 86400) {
|
|
112
|
+
type Breed @table {
|
|
116
113
|
id: Long @primaryKey
|
|
117
114
|
name: String @indexed
|
|
118
115
|
}
|
|
119
116
|
```
|
|
120
117
|
|
|
121
|
-
**
|
|
118
|
+
**Table with expiration, eviction, and custom scan interval:**
|
|
122
119
|
|
|
123
120
|
```graphql
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
# Expire after 5 minutes, evict after 1 hour, scan every 10 minutes
|
|
122
|
+
type WeatherCache @table(expiration: 300, eviction: 3300, scanInterval: 600) {
|
|
123
|
+
id: ID @primaryKey
|
|
124
|
+
temperature: Float
|
|
127
125
|
}
|
|
128
126
|
```
|
|
129
127
|
|
|
130
|
-
**
|
|
128
|
+
**Exported table with timestamps and a hidden field:**
|
|
131
129
|
|
|
132
130
|
```graphql
|
|
133
|
-
type
|
|
131
|
+
type Customer @table @export(name: "customers") {
|
|
134
132
|
id: Long @primaryKey
|
|
133
|
+
name: String @indexed
|
|
135
134
|
createdAt: Long @createdTime
|
|
136
135
|
updatedAt: Long @updatedTime
|
|
137
|
-
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
Internal — do not surface to external consumers.
|
|
139
|
+
"""
|
|
140
|
+
creditScore: Int @hidden
|
|
138
141
|
}
|
|
139
142
|
```
|
|
140
143
|
|
|
141
|
-
**
|
|
144
|
+
**Multiple `@table` argument combinations:**
|
|
142
145
|
|
|
143
146
|
```graphql
|
|
147
|
+
# Override table name
|
|
144
148
|
type Product @table(table: "products") {
|
|
145
149
|
id: Long @primaryKey
|
|
146
|
-
name: String
|
|
147
150
|
}
|
|
148
151
|
|
|
152
|
+
# Place in a specific database
|
|
153
|
+
type Order @table(database: "commerce") {
|
|
154
|
+
id: Long @primaryKey
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Auto-expire records after 1 hour
|
|
158
|
+
type Session @table(expiration: 3600) {
|
|
159
|
+
id: Long @primaryKey
|
|
160
|
+
userId: String
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Disable replication
|
|
149
164
|
type LocalRecord @table(replicate: false) {
|
|
150
165
|
id: Long @primaryKey
|
|
151
166
|
value: String
|
|
152
167
|
}
|
|
168
|
+
|
|
169
|
+
# Combine multiple arguments
|
|
170
|
+
type Event @table(database: "analytics", expiration: 86400) {
|
|
171
|
+
id: Long @primaryKey
|
|
172
|
+
name: String @indexed
|
|
173
|
+
}
|
|
153
174
|
```
|
|
154
175
|
|
|
155
176
|
## Notes
|
|
156
177
|
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
- `
|
|
160
|
-
-
|
|
161
|
-
-
|
|
178
|
+
- All tables default to the `"data"` database. When designing plugins or applications, use unique database names to avoid table naming collisions.
|
|
179
|
+
- Schemas are flexible by default — records may include properties beyond those declared. Use `@sealed` to prevent this.
|
|
180
|
+
- `expiration` marks a record stale; `eviction` controls how long after expiration the record is physically removed. Eviction does not remove records from secondary indexes — Harper fetches the full record on demand if an evicted record matches a query.
|
|
181
|
+
- `scanInterval` is clock-aligned to the server's local timezone, not startup-aligned. The eviction schedule is deterministic across restarts.
|
|
182
|
+
- If replication is disabled on a table and later re-enabled, writes made during the disabled period are not replicated retroactively.
|
|
183
|
+
- `@hidden` (on types or fields) is a metadata-visibility directive only. Use `attribute_permissions` on roles to enforce data access control.
|
|
184
|
+
- Disabling replication (`replicate: false`) and re-enabling it later will not catch up on writes made while replication was disabled.
|