@harperfast/skills 1.5.1 → 1.6.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.
|
@@ -2,41 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Guidelines for building scalable, secure, and performant applications on Harper. These practices cover everything from initial schema design to advanced deployment strategies.
|
|
4
4
|
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
1. [Schema & Data Design](#1-schema--data-design) — **HIGH**
|
|
10
|
-
- 1.1 [Adding Tables with Schemas](#11-adding-tables-with-schemas)
|
|
11
|
-
- 1.2 [Schema Design & Tooling](#12-schema-design--tooling)
|
|
12
|
-
- 1.3 [Defining Relationships](#13-defining-relationships)
|
|
13
|
-
- 1.4 [Vector Indexing](#14-vector-indexing)
|
|
14
|
-
- 1.5 [Using Blobs](#15-using-blobs)
|
|
15
|
-
- 1.6 [Handling Binary Data](#16-handling-binary-data)
|
|
16
|
-
2. [API & Communication](#2-api--communication) — **HIGH**
|
|
17
|
-
- 2.1 [Automatic REST APIs](#21-automatic-rest-apis)
|
|
18
|
-
- 2.2 [Querying REST APIs](#22-querying-rest-apis)
|
|
19
|
-
- 2.3 [Real-time Applications](#23-real-time-applications)
|
|
20
|
-
- 2.4 [Checking Authentication](#24-checking-authentication)
|
|
21
|
-
3. [Logic & Extension](#3-logic--extension) — **MEDIUM**
|
|
22
|
-
- 3.1 [Custom Resources](#31-custom-resources)
|
|
23
|
-
- 3.2 [Extending Table Resources](#32-extending-table-resources)
|
|
24
|
-
- 3.3 [Programmatic Table Requests](#33-programmatic-table-requests)
|
|
25
|
-
- 3.4 [TypeScript Type Stripping](#34-typescript-type-stripping)
|
|
26
|
-
- 3.5 [Caching](#35-caching)
|
|
27
|
-
4. [Infrastructure & Ops](#4-infrastructure--ops) — **MEDIUM**
|
|
28
|
-
- 4.1 [Creating Harper Applications](#41-creating-harper-applications)
|
|
29
|
-
- 4.2 [Creating a Fabric Account and Cluster](#42-creating-a-fabric-account-and-cluster)
|
|
30
|
-
- 4.3 [Deploying to Harper Fabric](#43-deploying-to-harper-fabric)
|
|
31
|
-
- 4.4 [Serving Web Content](#44-serving-web-content)
|
|
32
|
-
- 4.5 [Logging Best Practices](#45-logging-best-practices)
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
5
|
## 1. Schema & Data Design
|
|
37
6
|
|
|
38
|
-
**Impact: HIGH**
|
|
39
|
-
|
|
40
7
|
### 1.1 Adding Tables with Schemas
|
|
41
8
|
|
|
42
9
|
Instructions for the agent to follow when adding tables to a Harper database.
|
|
@@ -49,11 +16,21 @@ Use this skill when you need to define new data structures or modify existing on
|
|
|
49
16
|
|
|
50
17
|
1. **Create Dedicated Schema Files**: Prefer having a dedicated schema `.graphql` file for each table. Check the `config.yaml` file under `graphqlSchema.files` to see how it's configured. It typically accepts wildcards (e.g., `schemas/*.graphql`), but may be configured to point at a single file.
|
|
51
18
|
2. **Use Directives**: All available directives for defining your schema are defined in `node_modules/harper/schema.graphql`. Common directives include `@table`, `@export`, `@primaryKey`, `@indexed`, and `@relationship`.
|
|
52
|
-
3. **Define Relationships**: Link tables together using the `@relationship` directive.
|
|
53
|
-
4. **Enable Automatic APIs**: If you add `@table @export` to a schema type, Harper automatically sets up REST and WebSocket APIs for basic CRUD operations against that table. **Important**: REST endpoints also require `rest: true` in `config.yaml` — without it, `@export`ed tables will not respond to HTTP requests.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
19
|
+
3. **Define Relationships**: Link tables together using the `@relationship` directive. For more details, see the [Defining Relationships](defining-relationships.md) skill.
|
|
20
|
+
4. **Enable Automatic APIs**: If you add `@table @export` to a schema type, Harper automatically sets up REST and WebSocket APIs for basic CRUD operations against that table. **Important**: REST endpoints also require `rest: true` in `config.yaml` — without it, `@export`ed tables will not respond to HTTP requests. For a detailed list of available endpoints and how to use them, see the [Automatic REST APIs](automatic-apis.md) skill.
|
|
21
|
+
- `GET /{TableName}`: Describes the schema itself.
|
|
22
|
+
- `GET /{TableName}/`: Lists all records (supports filtering, sorting, and pagination via query parameters). See the [Querying REST APIs](querying-rest-apis.md) skill for details.
|
|
23
|
+
- `GET /{TableName}/{id}`: Retrieves a single record by its ID.
|
|
24
|
+
- `POST /{TableName}/`: Creates a new record.
|
|
25
|
+
- `PUT /{TableName}/{id}`: Updates an existing record.
|
|
26
|
+
- `PATCH /{TableName}/{id}`: Performs a partial update on a record.
|
|
27
|
+
- `DELETE /{TableName}/`: Deletes all records or filtered records.
|
|
28
|
+
- `DELETE /{TableName}/{id}`: Deletes a single record by its ID.
|
|
29
|
+
5. **Consider Table Extensions**: If you are going to [extend the table](./extending-tables.md) in your resources, then do not `@export` the table from the schema.
|
|
30
|
+
|
|
31
|
+
#### Examples
|
|
32
|
+
|
|
33
|
+
In a hypothetical `schemas/ExamplePerson.graphql`:
|
|
57
34
|
|
|
58
35
|
```graphql
|
|
59
36
|
type ExamplePerson @table @export {
|
|
@@ -129,16 +106,16 @@ my-harper-app/
|
|
|
129
106
|
|
|
130
107
|
### 1.3 Defining Relationships
|
|
131
108
|
|
|
132
|
-
|
|
109
|
+
Instructions for the agent to follow when defining relationships between Harper tables.
|
|
133
110
|
|
|
134
111
|
#### When to Use
|
|
135
112
|
|
|
136
|
-
Use this when you
|
|
113
|
+
Use this skill when you need to link data across different tables, enabling automatic joins and efficient related-data fetching via REST APIs.
|
|
137
114
|
|
|
138
115
|
#### How It Works
|
|
139
116
|
|
|
140
117
|
1. **Identify the Relationship Type**: Determine if it's one-to-one, many-to-one, or one-to-many.
|
|
141
|
-
2. **
|
|
118
|
+
2. **Use the `@relationship` Directive**: Apply it to a field in your GraphQL schema.
|
|
142
119
|
- **Many-to-One (Current table holds FK)**: Use `from`.
|
|
143
120
|
```graphql
|
|
144
121
|
type Book @table @export {
|
|
@@ -153,262 +130,782 @@ Use this when you have two or more tables that need to be logically linked (e.g.
|
|
|
153
130
|
}
|
|
154
131
|
```
|
|
155
132
|
3. **Query with Relationships**: Use dot syntax in REST API calls for filtering or the `select()` operator for including related data.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
```graphql
|
|
160
|
-
type Product @table @export {
|
|
161
|
-
id: ID @primaryKey
|
|
162
|
-
name: String
|
|
163
|
-
categoryId: ID
|
|
164
|
-
category: Category @relationship(from: "categoryId")
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
type Category @table @export {
|
|
168
|
-
id: ID @primaryKey
|
|
169
|
-
name: String
|
|
170
|
-
products: [Product] @relationship(to: "categoryId")
|
|
171
|
-
}
|
|
172
|
-
```
|
|
133
|
+
- Example Filter: `GET /Book/?author.name=Harper`
|
|
134
|
+
- Example Select: `GET /Author/?select(name,books(title))`
|
|
173
135
|
|
|
174
136
|
### 1.4 Vector Indexing
|
|
175
137
|
|
|
176
|
-
|
|
138
|
+
Instructions for the agent to follow when enabling and querying vector indexes for similarity search in Harper using the HNSW algorithm.
|
|
177
139
|
|
|
178
140
|
#### When to Use
|
|
179
141
|
|
|
180
|
-
|
|
142
|
+
Apply this rule when adding a vector index to a Harper table schema or writing similarity search queries against high-dimensional vector fields. Use it whenever you need approximate nearest-neighbor search, distance-threshold filtering, or distance-scored results.
|
|
181
143
|
|
|
182
144
|
#### How It Works
|
|
183
145
|
|
|
184
|
-
1. **
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
146
|
+
1. **Declare a vector index on a `[Float]` field**: Add `@indexed(type: "HNSW")` to any `[Float]` attribute in a `@table` type. See [adding-tables-with-schemas.md](adding-tables-with-schemas.md) for general schema setup.
|
|
147
|
+
|
|
148
|
+
```graphql
|
|
149
|
+
type Document @table {
|
|
150
|
+
id: Long @primaryKey
|
|
151
|
+
textEmbeddings: [Float] @indexed(type: "HNSW")
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
2. **Query by nearest neighbors using `sort`**: Call `Document.search()` with a `sort` object specifying `attribute` (the indexed field) and `target` (the query vector). Include `limit` to cap results.
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
let results = Document.search({
|
|
159
|
+
sort: { attribute: 'textEmbeddings', target: searchVector },
|
|
160
|
+
limit: 5,
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
3. **Combine HNSW with filter conditions**: Add a `conditions` array alongside `sort` to pre-filter records before ranking by similarity.
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
let results = Document.search({
|
|
168
|
+
conditions: [{ attribute: 'price', comparator: 'lt', value: 50 }],
|
|
169
|
+
sort: { attribute: 'textEmbeddings', target: searchVector },
|
|
170
|
+
limit: 5,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
4. **Filter by distance threshold**: Place `target` directly on a condition (alongside `attribute`, `comparator`, and `value`) to return only records whose distance to the target vector is below a threshold. Use this form to bound result quality by a similarity cutoff rather than ranking.
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
let results = Document.search({
|
|
178
|
+
conditions: {
|
|
179
|
+
attribute: 'textEmbeddings',
|
|
180
|
+
comparator: 'lt',
|
|
181
|
+
value: 0.1,
|
|
182
|
+
target: searchVector,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
5. **Include computed distance in results**: Add `'$distance'` to the `select` array to return the computed distance from the target vector alongside each record. `$distance` works in both `sort`-based and `conditions`-based queries.
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
let results = Document.search({
|
|
191
|
+
select: ['name', '$distance'],
|
|
192
|
+
sort: { attribute: 'textEmbeddings', target: searchVector },
|
|
193
|
+
limit: 5,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
6. **Tune HNSW parameters**: Pass additional parameters to `@indexed(type: "HNSW", ...)` to control index quality and performance:
|
|
188
198
|
|
|
189
|
-
|
|
199
|
+
| Parameter | Default | Description |
|
|
200
|
+
| ---------------------- | ----------------- | --------------------------------------------------------------------------------------------------- |
|
|
201
|
+
| `distance` | `"cosine"` | Distance function: `"euclidean"` or `"cosine"` (negative cosine similarity) |
|
|
202
|
+
| `efConstruction` | `100` | Max nodes explored during index construction. Higher = better recall, lower = better performance |
|
|
203
|
+
| `M` | `16` | Preferred connections per graph layer. Higher = more space, better recall for high-dimensional data |
|
|
204
|
+
| `optimizeRouting` | `0.5` | Heuristic aggressiveness for omitting redundant connections (0 = off, 1 = most aggressive) |
|
|
205
|
+
| `mL` | computed from `M` | Normalization factor for level generation |
|
|
206
|
+
| `efSearchConstruction` | `50` | Max nodes explored during search |
|
|
207
|
+
|
|
208
|
+
#### Examples
|
|
209
|
+
|
|
210
|
+
**Schema with custom HNSW parameters:**
|
|
190
211
|
|
|
191
212
|
```graphql
|
|
192
|
-
type Document @table
|
|
193
|
-
id:
|
|
194
|
-
|
|
195
|
-
|
|
213
|
+
type Document @table {
|
|
214
|
+
id: Long @primaryKey
|
|
215
|
+
textEmbeddings: [Float]
|
|
216
|
+
@indexed(type: "HNSW", distance: "euclidean", optimizeRouting: 0, efSearchConstruction: 100)
|
|
196
217
|
}
|
|
197
218
|
```
|
|
198
219
|
|
|
199
|
-
|
|
220
|
+
**Nearest-neighbor search with distance output:**
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
let results = Document.search({
|
|
224
|
+
select: ['name', '$distance'],
|
|
225
|
+
sort: { attribute: 'textEmbeddings', target: searchVector },
|
|
226
|
+
limit: 5,
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Distance-threshold filter (no ranking):**
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
let results = Document.search({
|
|
234
|
+
conditions: {
|
|
235
|
+
attribute: 'textEmbeddings',
|
|
236
|
+
comparator: 'lt',
|
|
237
|
+
value: 0.1,
|
|
238
|
+
target: searchVector,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### Notes
|
|
244
|
+
|
|
245
|
+
- The default `distance` function is `cosine`. To use Euclidean distance, set `distance: "euclidean"` in the `@indexed` directive.
|
|
246
|
+
- `efConstruction` controls index build quality; increase it to improve recall at the cost of slower indexing.
|
|
247
|
+
- `$distance` is a special field — prefix it with `$` exactly as shown; it is not a schema attribute.
|
|
248
|
+
- `target` is required in both `sort`-based and threshold-based condition queries to identify the reference vector for distance computation.
|
|
249
|
+
|
|
250
|
+
### 1.5 Using Blob Datatype
|
|
200
251
|
|
|
201
|
-
|
|
252
|
+
Instructions for the agent to follow when working with the Blob data type in Harper.
|
|
202
253
|
|
|
203
254
|
#### When to Use
|
|
204
255
|
|
|
205
|
-
Use this when you need to store large
|
|
256
|
+
Use this skill when you need to store unstructured or large binary data (media, documents) that is too large for standard JSON fields. Blobs provide efficient storage and integrated streaming support.
|
|
206
257
|
|
|
207
258
|
#### How It Works
|
|
208
259
|
|
|
209
|
-
1. **Define
|
|
210
|
-
|
|
211
|
-
|
|
260
|
+
1. **Define Blob Fields**: In your GraphQL schema, use the `Blob` type:
|
|
261
|
+
```graphql
|
|
262
|
+
type MyTable @table {
|
|
263
|
+
id: ID @primaryKey
|
|
264
|
+
data: Blob
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
2. **Create and Store Blobs**: Use `createBlob()` from Harper's globals to wrap Buffers or Streams:
|
|
268
|
+
```javascript
|
|
269
|
+
import { tables } from 'harper';
|
|
270
|
+
const blob = createBlob(largeBuffer);
|
|
271
|
+
await tables.MyTable.put('my-id', { data: blob });
|
|
272
|
+
```
|
|
273
|
+
3. **Use Streaming (Optional)**: For very large files, pass a stream to `createBlob()` to avoid loading the entire file into memory.
|
|
274
|
+
4. **Read Blob Data**: Retrieve the record and use `.bytes()` or streaming interfaces on the blob field:
|
|
275
|
+
```javascript
|
|
276
|
+
const record = await tables.MyTable.get('my-id');
|
|
277
|
+
const buffer = await record.data.bytes();
|
|
278
|
+
```
|
|
279
|
+
5. **Ensure Write Completion**: Use `saveBeforeCommit: true` in `createBlob` options if you need the blob fully written before the record is committed.
|
|
280
|
+
6. **Handle Errors**: Attach error listeners to the blob object to handle streaming failures.
|
|
212
281
|
|
|
213
282
|
### 1.6 Handling Binary Data
|
|
214
283
|
|
|
215
|
-
|
|
284
|
+
Instructions for the agent to follow when handling binary data in Harper.
|
|
216
285
|
|
|
217
286
|
#### When to Use
|
|
218
287
|
|
|
219
|
-
Use this when
|
|
288
|
+
Use this skill when you need to store binary files (images, audio, etc.) in the database or serve them back to clients via REST endpoints.
|
|
220
289
|
|
|
221
290
|
#### How It Works
|
|
222
291
|
|
|
223
|
-
1. **
|
|
224
|
-
|
|
225
|
-
|
|
292
|
+
1. **Store Binary Data**: In your resource's `post` or `put` method, convert incoming data to Buffers and then to Blobs using `createBlob` from Harper's globals. Include the MIME type if available:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
async post(target, record) {
|
|
296
|
+
if (record.data) {
|
|
297
|
+
record.data = createBlob(Buffer.from(record.data, record.encoding || 'base64'), {
|
|
298
|
+
type: record.contentType || 'application/octet-stream',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return super.post(target, record);
|
|
302
|
+
}
|
|
303
|
+
```
|
|
226
304
|
|
|
227
|
-
|
|
305
|
+
2. **Serve Binary Data**: In your resource's `get` method, return a response object with the appropriate `Content-Type` and the binary data in the `body`:
|
|
306
|
+
```typescript
|
|
307
|
+
async get(target) {
|
|
308
|
+
const record = await super.get(target);
|
|
309
|
+
if (record?.data) {
|
|
310
|
+
return {
|
|
311
|
+
status: 200,
|
|
312
|
+
headers: { 'Content-Type': record.data.type || 'application/octet-stream' },
|
|
313
|
+
body: record.data,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return record;
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
3. **Use the Blob Type**: Ensure your GraphQL schema uses the `Blob` scalar for binary fields.
|
|
228
320
|
|
|
229
321
|
## 2. API & Communication
|
|
230
322
|
|
|
231
|
-
|
|
323
|
+
### 2.1 Automatic APIs
|
|
324
|
+
|
|
325
|
+
Instructions for the agent to follow when utilizing Harper's automatic APIs.
|
|
232
326
|
|
|
233
|
-
|
|
327
|
+
#### When to Use
|
|
328
|
+
|
|
329
|
+
Use this skill when you want to interact with Harper tables via REST or WebSockets without writing custom resource logic. This is ideal for basic CRUD operations and real-time updates.
|
|
330
|
+
|
|
331
|
+
#### How It Works
|
|
332
|
+
|
|
333
|
+
1. **Enable REST in `config.yaml`**: REST endpoints are **not active by default**. You must explicitly enable them:
|
|
334
|
+
```yaml
|
|
335
|
+
rest: true
|
|
336
|
+
```
|
|
337
|
+
Without this, `@export`ed tables will not respond to HTTP requests.
|
|
338
|
+
2. **Enable Automatic APIs**: Ensure your GraphQL schema includes the `@export` directive for the table.
|
|
339
|
+
3. **Access REST Endpoints**: Use the standard endpoints for your table (Note: Paths are case-sensitive).
|
|
340
|
+
4. **Use Automatic WebSockets**: Connect to `wss://your-harper-instance/{TableName}` to receive events whenever updates are made to that table. This is the easiest way to add real-time capabilities. (Use `ws://` for local development without SSL). For more complex needs, see [Real-time Apps](real-time-apps.md).
|
|
341
|
+
5. **Apply Filtering and Querying**: Use query parameters with `GET /{TableName}/` and `DELETE /{TableName}/`. See the [Querying REST APIs](querying-rest-apis.md) skill for advanced details.
|
|
342
|
+
6. **Customize if Needed**: If the automatic APIs don't meet your requirements, [customize the resources](./custom-resources.md).
|
|
343
|
+
|
|
344
|
+
#### Examples
|
|
345
|
+
|
|
346
|
+
##### Schema Configuration
|
|
234
347
|
|
|
235
|
-
|
|
348
|
+
```graphql
|
|
349
|
+
type MyTable @table @export {
|
|
350
|
+
id: ID @primaryKey
|
|
351
|
+
name: String
|
|
352
|
+
}
|
|
353
|
+
```
|
|
236
354
|
|
|
237
|
-
|
|
355
|
+
##### Common REST Operations
|
|
238
356
|
|
|
239
|
-
- `GET /
|
|
240
|
-
-
|
|
241
|
-
- `
|
|
242
|
-
- `POST /{TableName}/`: Creates a record.
|
|
243
|
-
- `PUT /{TableName}/{id}`: Updates a record.
|
|
244
|
-
- `PATCH /{TableName}/{id}`: Partial update.
|
|
245
|
-
- `DELETE /{TableName}/`: Deletes records.
|
|
246
|
-
- `DELETE /{TableName}/{id}`: Deletes by ID.
|
|
357
|
+
- **List Records**: `GET /MyTable/`
|
|
358
|
+
- **Create Record**: `POST /MyTable/`
|
|
359
|
+
- **Update Record**: `PATCH /MyTable/{id}`
|
|
247
360
|
|
|
248
361
|
### 2.2 Querying REST APIs
|
|
249
362
|
|
|
250
|
-
|
|
363
|
+
Instructions for the agent to follow when querying Harper's REST APIs.
|
|
364
|
+
|
|
365
|
+
#### When to Use
|
|
366
|
+
|
|
367
|
+
Use this skill when you need to perform advanced data retrieval (filtering, sorting, pagination, joins) using Harper's automatic REST endpoints.
|
|
251
368
|
|
|
252
|
-
####
|
|
369
|
+
#### How It Works
|
|
253
370
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
371
|
+
1. **Basic Filtering**: Use attribute names as query parameters: `GET /Table/?key=value`.
|
|
372
|
+
2. **Use Comparison Operators**: Append operators like `gt`, `ge`, `lt`, `le`, `ne` using FIQL-style syntax: `GET /Table/?price=gt=100`.
|
|
373
|
+
3. **Apply Logic and Grouping**: Use `&` for AND, `|` for OR, and `()` for grouping: `GET /Table/?(rating=5|featured=true)&price=lt=50`.
|
|
374
|
+
4. **Select Specific Fields**: Use `select()` to limit returned attributes: `GET /Table/?select(name,price)`.
|
|
375
|
+
5. **Paginate Results**: Use `limit(count)` or `limit(offset, count)` to set the number of records to return and skip.
|
|
376
|
+
- Example (first 10): `GET /Table/?limit(10)`
|
|
377
|
+
- Example (skip 20, return 10): `GET /Table/?limit(20, 10)`
|
|
378
|
+
6. **Sort Results**: Use `sort()` with `+` (asc) or `-` (desc) before the field name. Avoid `sort=field` format.
|
|
379
|
+
- Example (asc): `GET /Table/?sort(+name)`
|
|
380
|
+
- Example (desc): `GET /Table/?sort(-price)`
|
|
381
|
+
- Example (combined): `GET /Table/?sort(-price,+name)`
|
|
382
|
+
7. **Query Relationships**: Use dot syntax for tables linked with `@relationship`: `GET /Book/?author.name=Harper`.
|
|
260
383
|
|
|
261
384
|
### 2.3 Real-time Applications
|
|
262
385
|
|
|
263
|
-
|
|
386
|
+
Instructions for the agent to follow when building real-time applications in Harper.
|
|
264
387
|
|
|
265
388
|
#### When to Use
|
|
266
389
|
|
|
267
|
-
Use this
|
|
390
|
+
Use this skill when you need to stream live updates to clients, implement chat features, or provide real-time data synchronization between the database and a frontend.
|
|
268
391
|
|
|
269
392
|
#### How It Works
|
|
270
393
|
|
|
271
|
-
1. **
|
|
272
|
-
2. **
|
|
273
|
-
3. **Pub/Sub**: Use
|
|
394
|
+
1. **Check Automatic WebSockets**: If you only need to stream table changes, use [Automatic APIs](automatic-apis.md) which provide a WebSocket endpoint for every `@export`ed table.
|
|
395
|
+
2. **Implement `connect` in a Resource**: For custom bi-directional logic, implement the `connect` method.
|
|
396
|
+
3. **Use Pub/Sub**: Use `tables.TableName.subscribe(query)` to listen for specific data changes and stream them to the client.
|
|
397
|
+
4. **Handle SSE**: Ensure your `connect` method gracefully handles cases where `incomingMessages` is null (Server-Sent Events).
|
|
398
|
+
5. **Connect from Client**: Use standard WebSockets (`new WebSocket('wss://...')`) to connect to your resource endpoint. Ensure you use the appropriate scheme (`ws://` for HTTP, `wss://` for HTTPS).
|
|
399
|
+
|
|
400
|
+
#### Examples
|
|
401
|
+
|
|
402
|
+
##### Bi-directional WebSocket Resource
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { Resource, tables } from 'harper';
|
|
406
|
+
|
|
407
|
+
export class MySocket extends Resource {
|
|
408
|
+
async *connect(target, incomingMessages) {
|
|
409
|
+
// Subscribe to table changes
|
|
410
|
+
const subscription = await tables.MyTable.subscribe(target);
|
|
411
|
+
if (!incomingMessages) {
|
|
412
|
+
return subscription; // SSE mode
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Handle incoming client messages
|
|
416
|
+
for await (let message of incomingMessages) {
|
|
417
|
+
yield { received: message };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
274
422
|
|
|
275
423
|
### 2.4 Checking Authentication
|
|
276
424
|
|
|
277
|
-
|
|
425
|
+
Instructions for the agent to follow when handling authentication and sessions.
|
|
278
426
|
|
|
279
427
|
#### When to Use
|
|
280
428
|
|
|
281
|
-
Use this to
|
|
429
|
+
Use this skill when you need to implement sign-in/sign-out functionality, protect specific resource endpoints, or identify the currently logged-in user in a Harper application.
|
|
282
430
|
|
|
283
431
|
#### How It Works
|
|
284
432
|
|
|
285
|
-
1. **
|
|
286
|
-
|
|
287
|
-
|
|
433
|
+
1. **Configure Harper for Sessions**: Ensure `harper-config.yaml` has sessions enabled and local auto-authorization disabled for testing:
|
|
434
|
+
```yaml
|
|
435
|
+
authentication:
|
|
436
|
+
authorizeLocal: false
|
|
437
|
+
enableSessions: true
|
|
438
|
+
```
|
|
439
|
+
2. **Implement Sign In**: Use `this.getContext().login(username, password)` to create a session:
|
|
440
|
+
```typescript
|
|
441
|
+
async post(_target, data) {
|
|
442
|
+
const context = this.getContext();
|
|
443
|
+
try {
|
|
444
|
+
await context.login(data.username, data.password);
|
|
445
|
+
} catch {
|
|
446
|
+
return new Response('Invalid credentials', { status: 403 });
|
|
447
|
+
}
|
|
448
|
+
return new Response('Logged in', { status: 200 });
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
3. **Identify Current User**: Use `this.getCurrentUser()` to access session data:
|
|
452
|
+
```typescript
|
|
453
|
+
async get() {
|
|
454
|
+
const user = this.getCurrentUser?.();
|
|
455
|
+
if (!user) return new Response(null, { status: 401 });
|
|
456
|
+
return { username: user.username, role: user.role };
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
4. **Implement Sign Out**: Use `this.getContext().logout()` or delete the session from context:
|
|
460
|
+
```typescript
|
|
461
|
+
async post() {
|
|
462
|
+
const context = this.getContext();
|
|
463
|
+
await context.session?.delete?.(context.session.id);
|
|
464
|
+
return new Response('Logged out', { status: 200 });
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
5. **Protect Routes**: In your Resource, use `allowRead()`, `allowUpdate()`, etc., to enforce authorization logic based on `this.getCurrentUser()`. For privileged actions, verify `user.role.permission.super_user`.
|
|
288
468
|
|
|
289
|
-
|
|
469
|
+
#### Examples
|
|
290
470
|
|
|
291
|
-
|
|
471
|
+
##### Sign In Implementation
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
async post(_target, data) {
|
|
475
|
+
const context = this.getContext();
|
|
476
|
+
try {
|
|
477
|
+
await context.login(data.username, data.password);
|
|
478
|
+
} catch {
|
|
479
|
+
return new Response('Invalid credentials', { status: 403 });
|
|
480
|
+
}
|
|
481
|
+
return new Response('Logged in', { status: 200 });
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
##### Identify Current User
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
async get() {
|
|
489
|
+
const user = this.getCurrentUser?.();
|
|
490
|
+
if (!user) return new Response(null, { status: 401 });
|
|
491
|
+
return { username: user.username, role: user.role };
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
##### Sign Out Implementation
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
async post() {
|
|
499
|
+
const context = this.getContext();
|
|
500
|
+
await context.session?.delete?.(context.session.id);
|
|
501
|
+
return new Response('Logged out', { status: 200 });
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
#### Status code conventions used here
|
|
506
|
+
|
|
507
|
+
- 200: Successful operation. For `GET /me`, a `200` with empty body means “not signed in”.
|
|
508
|
+
- 400: Missing required fields (e.g., username/password on sign-in).
|
|
509
|
+
- 401: No current session for an action that requires one (e.g., sign out when not signed in).
|
|
510
|
+
- 403: Authenticated but not authorized (bad credentials on login attempt, or insufficient privileges).
|
|
511
|
+
|
|
512
|
+
#### Client considerations
|
|
513
|
+
|
|
514
|
+
- Sessions are cookie-based; the server handles setting and reading the cookie via Harper. If you make cross-origin requests, ensure the appropriate `credentials` mode and CORS settings.
|
|
515
|
+
- If developing locally, double-check the server config still has `authentication.authorizeLocal: false` to avoid accidental superuser bypass.
|
|
516
|
+
|
|
517
|
+
#### Token-based auth (JWT + refresh token) for non-browser clients
|
|
292
518
|
|
|
293
|
-
**
|
|
519
|
+
Cookie-backed sessions are great for browser flows. For CLI tools, mobile apps, or other non-browser clients, it’s often easier to use **explicit tokens**:
|
|
520
|
+
|
|
521
|
+
- **JWT (`operation_token`)**: short-lived bearer token used to authorize API requests.
|
|
522
|
+
- **Refresh token (`refresh_token`)**: longer-lived token used to mint a new JWT when it expires.
|
|
523
|
+
|
|
524
|
+
This project includes two Resource patterns for that flow:
|
|
525
|
+
|
|
526
|
+
##### Issuing tokens: `IssueTokens`
|
|
527
|
+
|
|
528
|
+
**Description / use case:** Generate `{ refreshToken, jwt }` either:
|
|
529
|
+
|
|
530
|
+
- with an existing Authorization token (either Basic Auth or a JWT) and you want to issue new tokens, or
|
|
531
|
+
- from an explicit `{ username, password }` payload (useful for direct “login” from a CLI/mobile client).
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
export class IssueTokens extends Resource {
|
|
535
|
+
static loadAsInstance = false;
|
|
536
|
+
|
|
537
|
+
async get(target) {
|
|
538
|
+
const { refresh_token: refreshToken, operation_token: jwt } =
|
|
539
|
+
await databases.system.hdb_user.operation(
|
|
540
|
+
{ operation: 'create_authentication_tokens' },
|
|
541
|
+
this.getContext(),
|
|
542
|
+
);
|
|
543
|
+
return { refreshToken, jwt };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async post(target, data) {
|
|
547
|
+
if (!data.username || !data.password) {
|
|
548
|
+
throw new Error('username and password are required');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const { refresh_token: refreshToken, operation_token: jwt } =
|
|
552
|
+
await databases.system.hdb_user.operation({
|
|
553
|
+
operation: 'create_authentication_tokens',
|
|
554
|
+
username: data.username,
|
|
555
|
+
password: data.password,
|
|
556
|
+
});
|
|
557
|
+
return { refreshToken, jwt };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Recommended documentation notes to include:**
|
|
563
|
+
|
|
564
|
+
- `GET` variant: intended for “I already have an Authorization token, give me new tokens”.
|
|
565
|
+
- `POST` variant: intended for “I have credentials, give me tokens”.
|
|
566
|
+
- Response shape:
|
|
567
|
+
- `refreshToken`: store securely (long-lived).
|
|
568
|
+
- `jwt`: attach to requests (short-lived).
|
|
569
|
+
|
|
570
|
+
##### Refreshing a JWT: `RefreshJWT`
|
|
571
|
+
|
|
572
|
+
**Description / use case:** When the JWT expires, the client uses the refresh token to get a new JWT without re-supplying username/password.
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
export class RefreshJWT extends Resource {
|
|
576
|
+
static loadAsInstance = false;
|
|
577
|
+
|
|
578
|
+
async post(target, data) {
|
|
579
|
+
if (!data.refreshToken) {
|
|
580
|
+
throw new Error('refreshToken is required');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const { operation_token: jwt } = await databases.system.hdb_user.operation({
|
|
584
|
+
operation: 'refresh_operation_token',
|
|
585
|
+
refresh_token: data.refreshToken,
|
|
586
|
+
});
|
|
587
|
+
return { jwt };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Recommended documentation notes to include:**
|
|
593
|
+
|
|
594
|
+
- Requires `refreshToken` in the request body.
|
|
595
|
+
- Returns a new `{ jwt }`.
|
|
596
|
+
- If refresh fails (expired/revoked), client must re-authenticate (e.g., call `IssueTokens.post` again).
|
|
597
|
+
|
|
598
|
+
##### Suggested client flow (high-level)
|
|
599
|
+
|
|
600
|
+
1. **Sign in (token flow)**
|
|
601
|
+
- POST /IssueTokens/ with a body of `{ "username": "your username", "password": "your password" }` or GET /IssueTokens/ with an existing Authorization token.
|
|
602
|
+
- Receive `{ jwt, refreshToken }` in the response
|
|
603
|
+
2. **Call protected APIs**
|
|
604
|
+
- Send the JWT with each request in the Authorization header (as your auth mechanism expects)
|
|
605
|
+
3. **JWT expires**
|
|
606
|
+
- POST /RefreshJWT/ with a body of `{ "refreshToken": "your refresh token" }`.
|
|
607
|
+
- Receive `{ jwt }` in the response and continue
|
|
608
|
+
|
|
609
|
+
#### Quick checklist
|
|
610
|
+
|
|
611
|
+
- [ ] Public endpoints explicitly `allowRead`/`allowCreate` as needed.
|
|
612
|
+
- [ ] Sign-in uses `context.login` and handles 400/403 correctly.
|
|
613
|
+
- [ ] Protected routes call `ensureSuperUser(this.getCurrentUser())` (or another role check) before doing work.
|
|
614
|
+
- [ ] Sign-out verifies a session and deletes it.
|
|
615
|
+
- [ ] `authentication.authorizeLocal` is `false` and `enableSessions` is `true` in Harper config.
|
|
616
|
+
- [ ] If using tokens: `IssueTokens` issues `{ jwt, refreshToken }`, `RefreshJWT` refreshes `{ jwt }` with a `refreshToken`.
|
|
617
|
+
|
|
618
|
+
## 3. Logic & Extension
|
|
294
619
|
|
|
295
620
|
### 3.1 Custom Resources
|
|
296
621
|
|
|
297
|
-
|
|
622
|
+
Instructions for the agent to follow when creating custom resources in Harper.
|
|
623
|
+
|
|
624
|
+
#### When to Use
|
|
625
|
+
|
|
626
|
+
Use this skill when the automatic CRUD operations provided by `@table @export` are insufficient, and you need custom logic, third-party API integration, or specialized data handling for your REST endpoints.
|
|
298
627
|
|
|
299
628
|
#### How It Works
|
|
300
629
|
|
|
301
|
-
1. **
|
|
302
|
-
2. **
|
|
303
|
-
3. **
|
|
304
|
-
|
|
630
|
+
1. **Check if a Custom Resource is Necessary**: Verify if [Automatic APIs](./automatic-apis.md) or [Extending Tables](./extending-tables.md) can satisfy the requirement first.
|
|
631
|
+
2. **Create the Resource File**: Create a `.ts` or `.js` file in the directory specified by `jsResource` in `config.yaml` (typically `resources/`).
|
|
632
|
+
3. **Define the Resource Class**: Export a class extending `Resource` from `harper`:
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
import { type RequestTargetOrId, Resource } from 'harper';
|
|
636
|
+
|
|
637
|
+
export class MyResource extends Resource {
|
|
638
|
+
async get(target?: RequestTargetOrId) {
|
|
639
|
+
return { message: 'Hello from custom GET!' };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
4. **Implement HTTP Methods**: Add methods like `get`, `post`, `put`, `patch`, or `delete` to handle corresponding requests.
|
|
645
|
+
5. **Route Nesting and Naming**: You can control the URL structure by how you export your resources:
|
|
305
646
|
- **Direct Class Export**: `export class Foo extends Resource` creates endpoints at `/Foo/`. Class names are case-sensitive in the URL.
|
|
306
647
|
- **Nested Objects**: `export const Bar = { Foo };` creates endpoints at `/Bar/Foo/`.
|
|
307
648
|
- **Lowercase and Hyphens**: Use object keys to define custom paths: `export const bar = { 'foo-baz': Foo };` exposes endpoints at `/bar/foo-baz/`.
|
|
308
|
-
|
|
649
|
+
6. **Access Tables (Optional)**: Import and use the `tables` object to interact with your data:
|
|
650
|
+
```typescript
|
|
651
|
+
import { tables } from 'harper';
|
|
652
|
+
// ... inside a method
|
|
653
|
+
const results = await tables.MyTable.list();
|
|
654
|
+
```
|
|
655
|
+
7. **Configure Loading**: Ensure `config.yaml` points to your resource files (e.g., `jsResource: { files: 'resources/*.ts' }`).
|
|
656
|
+
|
|
657
|
+
### 3.2 Extending Tables
|
|
658
|
+
|
|
659
|
+
Instructions for the agent to follow when extending table resources in Harper.
|
|
309
660
|
|
|
310
|
-
|
|
661
|
+
#### When to Use
|
|
311
662
|
|
|
312
|
-
|
|
663
|
+
Use this skill when you need to add custom validation, side effects (like webhooks), data transformation, or custom access control to the standard CRUD operations of a Harper table.
|
|
313
664
|
|
|
314
665
|
#### How It Works
|
|
315
666
|
|
|
316
|
-
1. **Define
|
|
317
|
-
|
|
318
|
-
|
|
667
|
+
1. **Define the Table in GraphQL**: In your `.graphql` schema, define the table using the `@table` directive. **Do not** use `@export` if you plan to extend it.
|
|
668
|
+
```graphql
|
|
669
|
+
type MyTable @table {
|
|
670
|
+
id: ID @primaryKey
|
|
671
|
+
name: String
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
2. **Create the Extension File**: Create a `.ts` file in your `resources/` directory.
|
|
675
|
+
3. **Extend the Table Resource**: Export a class that extends `tables.YourTableName`:
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
import { type RequestTargetOrId, tables } from 'harper';
|
|
679
|
+
|
|
680
|
+
export class MyTable extends tables.MyTable {
|
|
681
|
+
async post(target: RequestTargetOrId, record: any) {
|
|
682
|
+
// Custom logic here
|
|
683
|
+
if (!record.name) {
|
|
684
|
+
throw new Error('Name required');
|
|
685
|
+
}
|
|
686
|
+
return super.post(target, record);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
4. **Override Methods**: Override `get`, `post`, `put`, `patch`, or `delete` as needed. Always call `super[method]` to maintain default Harper functionality unless you intend to replace it entirely.
|
|
692
|
+
5. **Implement Logic**: Use overrides for validation, side effects, or transforming data before/after database operations.
|
|
319
693
|
|
|
320
694
|
### 3.3 Programmatic Table Requests
|
|
321
695
|
|
|
322
|
-
|
|
696
|
+
Instructions for the agent to follow when interacting with Harper tables via code.
|
|
323
697
|
|
|
324
|
-
####
|
|
698
|
+
#### When to Use
|
|
325
699
|
|
|
326
|
-
|
|
700
|
+
Use this skill when you need to perform database operations (CRUD, search, subscribe) from within Harper Resources or scripts.
|
|
327
701
|
|
|
328
|
-
|
|
702
|
+
#### How It Works
|
|
329
703
|
|
|
330
|
-
|
|
704
|
+
1. **Access the Table**: Use the global `tables` object followed by your table name (e.g., `tables.MyTable`).
|
|
705
|
+
2. **Perform CRUD Operations**:
|
|
706
|
+
- **Get**: `await tables.MyTable.get(id)` for a single record or `await tables.MyTable.get({ conditions: [...] })` for multiple.
|
|
707
|
+
- **Create**: `await tables.MyTable.post(record)` (auto-generates ID) or `await tables.MyTable.put(id, record)`.
|
|
708
|
+
- **Update**: `await tables.MyTable.patch(id, partialRecord)` for partial updates.
|
|
709
|
+
- **Delete**: `await tables.MyTable.delete(id)`.
|
|
710
|
+
3. **Use Updatable Records for Atomic Ops**: Call `update(id)` to get a reference, then use `addTo` or `subtractFrom` for atomic increments/decrements:
|
|
711
|
+
```typescript
|
|
712
|
+
const stats = await tables.Stats.update('daily');
|
|
713
|
+
stats.addTo('viewCount', 1);
|
|
714
|
+
```
|
|
715
|
+
4. **Search and Stream**: Use `search(query)` for efficient streaming of large result sets:
|
|
716
|
+
```typescript
|
|
717
|
+
for await (const record of tables.MyTable.search({ conditions: [...] })) {
|
|
718
|
+
// process record
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
See the [Query Conditions](#query-conditions) section below for the full query object reference.
|
|
722
|
+
5. **Real-time Subscriptions**: Use `subscribe(query)` to listen for changes:
|
|
723
|
+
```typescript
|
|
724
|
+
for await (const event of tables.MyTable.subscribe(query)) {
|
|
725
|
+
// handle event
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
6. **Publish Events**: Use `publish(id, message)` to trigger subscriptions without necessarily persisting data.
|
|
729
|
+
|
|
730
|
+
#### Query Conditions
|
|
731
|
+
|
|
732
|
+
When passing a query to `search()`, `get()`, or `subscribe()`, use a query object with a `conditions` array.
|
|
733
|
+
|
|
734
|
+
##### Condition Object Shape
|
|
735
|
+
|
|
736
|
+
| Property | Description |
|
|
737
|
+
| ------------ | ------------------------------------------------------------------------------------------ |
|
|
738
|
+
| `attribute` | Field name, or array of field names to traverse a relationship (e.g., `['brand', 'name']`) |
|
|
739
|
+
| `value` | The value to compare against |
|
|
740
|
+
| `comparator` | One of the comparator strings below (default: `equals`) |
|
|
741
|
+
| `operator` | `and` (default) or `or` — applies to a nested `conditions` block |
|
|
742
|
+
| `conditions` | Nested array of condition objects for complex AND/OR logic |
|
|
743
|
+
|
|
744
|
+
##### Comparator Values
|
|
745
|
+
|
|
746
|
+
Use these exact strings — incorrect comparator names will silently fail or error:
|
|
747
|
+
|
|
748
|
+
| Comparator | Meaning |
|
|
749
|
+
| -------------------- | ---------------------------------------------------------- |
|
|
750
|
+
| `equals` | Exact match (default) |
|
|
751
|
+
| `not_equal` | Not equal |
|
|
752
|
+
| `greater_than` | `>` |
|
|
753
|
+
| `greater_than_equal` | `>=` |
|
|
754
|
+
| `less_than` | `<` |
|
|
755
|
+
| `less_than_equal` | `<=` |
|
|
756
|
+
| `starts_with` | String starts with value |
|
|
757
|
+
| `contains` | String contains value |
|
|
758
|
+
| `ends_with` | String ends with value |
|
|
759
|
+
| `between` | Value is between two bounds (pass `value` as `[min, max]`) |
|
|
760
|
+
|
|
761
|
+
##### Query Object Parameters
|
|
762
|
+
|
|
763
|
+
| Property | Description |
|
|
764
|
+
| ------------ | ------------------------------------------------------------------------------------ |
|
|
765
|
+
| `conditions` | Array of condition objects |
|
|
766
|
+
| `limit` | Maximum number of records to return |
|
|
767
|
+
| `offset` | Number of records to skip (for pagination) |
|
|
768
|
+
| `select` | Array of attribute names to return; supports `$id` and `$updatedtime` |
|
|
769
|
+
| `sort` | Object with `attribute`, `descending` (bool), and optional `next` for secondary sort |
|
|
770
|
+
|
|
771
|
+
##### Examples
|
|
772
|
+
|
|
773
|
+
**Simple filter:**
|
|
774
|
+
|
|
775
|
+
```javascript
|
|
776
|
+
for await (const record of tables.Product.search({
|
|
777
|
+
conditions: [{ attribute: 'price', comparator: 'less_than', value: 100 }],
|
|
778
|
+
limit: 20,
|
|
779
|
+
})) { ... }
|
|
780
|
+
```
|
|
331
781
|
|
|
332
|
-
|
|
782
|
+
**AND + nested OR:**
|
|
783
|
+
|
|
784
|
+
```javascript
|
|
785
|
+
for await (const record of tables.Product.search({
|
|
786
|
+
conditions: [
|
|
787
|
+
{ attribute: 'price', comparator: 'less_than', value: 100 },
|
|
788
|
+
{
|
|
789
|
+
operator: 'or',
|
|
790
|
+
conditions: [
|
|
791
|
+
{ attribute: 'rating', comparator: 'greater_than', value: 4 },
|
|
792
|
+
{ attribute: 'featured', value: true },
|
|
793
|
+
],
|
|
794
|
+
},
|
|
795
|
+
],
|
|
796
|
+
})) { ... }
|
|
797
|
+
```
|
|
333
798
|
|
|
334
|
-
|
|
799
|
+
**Relationship traversal:**
|
|
335
800
|
|
|
336
|
-
|
|
801
|
+
```javascript
|
|
802
|
+
for await (const record of tables.Book.search({
|
|
803
|
+
conditions: [{ attribute: ['brand', 'name'], comparator: 'equals', value: 'Harper' }],
|
|
804
|
+
})) { ... }
|
|
805
|
+
```
|
|
337
806
|
|
|
338
|
-
|
|
807
|
+
**Sort and paginate:**
|
|
339
808
|
|
|
340
|
-
|
|
809
|
+
```javascript
|
|
810
|
+
for await (const record of tables.Product.search({
|
|
811
|
+
conditions: [{ attribute: 'inStock', value: true }],
|
|
812
|
+
sort: { attribute: 'price', descending: false },
|
|
813
|
+
limit: 10,
|
|
814
|
+
offset: 20,
|
|
815
|
+
})) { ... }
|
|
816
|
+
```
|
|
341
817
|
|
|
342
|
-
|
|
343
|
-
- **Distributed**: For scaling across multiple nodes in Harper Fabric.
|
|
818
|
+
#### Cautions
|
|
344
819
|
|
|
345
|
-
|
|
820
|
+
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.
|
|
346
821
|
|
|
347
|
-
|
|
822
|
+
### 3.4 TypeScript Type Stripping
|
|
348
823
|
|
|
349
|
-
|
|
824
|
+
Instructions for the agent to follow when using TypeScript in Harper.
|
|
350
825
|
|
|
351
|
-
|
|
826
|
+
#### When to Use
|
|
352
827
|
|
|
353
|
-
|
|
828
|
+
Use this skill when you want to write Harper Resources in TypeScript and have them execute directly in Node.js without an intermediate build or compilation step.
|
|
354
829
|
|
|
355
|
-
####
|
|
830
|
+
#### How It Works
|
|
356
831
|
|
|
357
|
-
|
|
832
|
+
1. **Verify Node.js Version**: Ensure you are using Node.js v22.6.0 or higher.
|
|
833
|
+
2. **Name Files with `.ts`**: Create your resource files in the `resources/` directory with a `.ts` extension.
|
|
834
|
+
3. **Use TypeScript Syntax**: Write your resource classes using standard TypeScript (interfaces, types, etc.).
|
|
835
|
+
```typescript
|
|
836
|
+
import { Resource } from 'harper';
|
|
837
|
+
export class MyResource extends Resource {
|
|
838
|
+
async get(): Promise<{ message: string }> {
|
|
839
|
+
return { message: 'Running TS directly!' };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
4. **Use Explicit Extensions in Imports**: When importing other local modules, include the `.ts` extension: `import { helper } from './helper.ts'`.
|
|
844
|
+
5. **Configure `config.yaml`**: Ensure `jsResource` points to your `.ts` files:
|
|
845
|
+
```yaml
|
|
846
|
+
jsResource:
|
|
847
|
+
files: 'resources/*.ts'
|
|
848
|
+
```
|
|
358
849
|
|
|
359
|
-
|
|
850
|
+
### 3.5 Caching
|
|
360
851
|
|
|
361
|
-
|
|
852
|
+
Instructions for the agent to follow when implementing caching in Harper.
|
|
853
|
+
|
|
854
|
+
#### When to Use
|
|
362
855
|
|
|
363
|
-
|
|
856
|
+
Use this skill when you need high-performance, low-latency storage for data from external sources. It's ideal for reducing API calls to third-party services, preventing cache stampedes, and making external data queryable as if it were native Harper tables.
|
|
364
857
|
|
|
365
|
-
|
|
366
|
-
npm create harper@latest
|
|
367
|
-
```
|
|
858
|
+
#### How It Works
|
|
368
859
|
|
|
369
|
-
**
|
|
860
|
+
1. **Configure a Cache Table**: Define a table in your `schema.graphql` with an `expiration` (in seconds).
|
|
861
|
+
2. **Define an External Source**: Create a Resource class that fetches the data from your source.
|
|
862
|
+
3. **Attach Source to Table**: Use `sourcedFrom` to link your resource to the table.
|
|
863
|
+
4. **Implement Active Caching (Optional)**: Use `subscribe()` for proactive updates. See [Real-Time Apps](real-time-apps.md).
|
|
864
|
+
5. **Implement Write-Through Caching (Optional)**: Define `put` or `post` in your resource to propagate updates upstream.
|
|
370
865
|
|
|
371
|
-
|
|
372
|
-
pnpm create harper@latest
|
|
373
|
-
```
|
|
866
|
+
#### Examples
|
|
374
867
|
|
|
375
|
-
|
|
868
|
+
##### Schema Configuration
|
|
376
869
|
|
|
377
|
-
```
|
|
378
|
-
|
|
870
|
+
```graphql
|
|
871
|
+
type MyCache @table(expiration: 3600) @export {
|
|
872
|
+
id: ID @primaryKey
|
|
873
|
+
}
|
|
379
874
|
```
|
|
380
875
|
|
|
381
|
-
|
|
876
|
+
##### Resource Implementation
|
|
382
877
|
|
|
383
|
-
|
|
878
|
+
```js
|
|
879
|
+
import { Resource, tables } from 'harper';
|
|
384
880
|
|
|
385
|
-
|
|
881
|
+
export class ThirdPartyAPI extends Resource {
|
|
882
|
+
async get() {
|
|
883
|
+
const id = this.getId();
|
|
884
|
+
const response = await fetch(`https://api.example.com/items/${id}`);
|
|
885
|
+
if (!response.ok) {
|
|
886
|
+
throw new Error('Source fetch failed');
|
|
887
|
+
}
|
|
888
|
+
return await response.json();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
386
891
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
4. **Set Credentials**: During setup, set the cluster username and password to finish configuring it.
|
|
391
|
-
5. **Get Application URL**: Navigate to the **Config** tab and copy the **Application URL**.
|
|
392
|
-
6. **Configure Environment**: Update your `.env` file or GitHub Actions secrets with these cluster-specific credentials:
|
|
393
|
-
```bash
|
|
394
|
-
CLI_TARGET_USERNAME='YOUR_CLUSTER_USERNAME'
|
|
395
|
-
CLI_TARGET_PASSWORD='YOUR_CLUSTER_PASSWORD'
|
|
396
|
-
CLI_TARGET='YOUR_CLUSTER_URL'
|
|
397
|
-
```
|
|
892
|
+
// Attach source to table
|
|
893
|
+
tables.MyCache.sourcedFrom(ThirdPartyAPI);
|
|
894
|
+
```
|
|
398
895
|
|
|
399
|
-
|
|
896
|
+
## 4. Infrastructure & Ops
|
|
897
|
+
|
|
898
|
+
### 4.1 Deploying to Harper Fabric
|
|
400
899
|
|
|
401
|
-
|
|
900
|
+
Instructions for the agent to follow when deploying to Harper Fabric.
|
|
402
901
|
|
|
403
|
-
####
|
|
902
|
+
#### When to Use
|
|
404
903
|
|
|
405
|
-
|
|
406
|
-
- **Automatic Sync**: Data is synced across the fabric automatically.
|
|
407
|
-
- **Free Tier**: Start for free and scale as you grow.
|
|
904
|
+
Use this skill when you are ready to move your Harper application from local development to a cloud-hosted environment.
|
|
408
905
|
|
|
409
906
|
#### How It Works
|
|
410
907
|
|
|
411
|
-
1. **Sign up**: Follow the [
|
|
908
|
+
1. **Sign up**: Follow the [creating-a-fabric-account-and-cluster](creating-a-fabric-account-and-cluster.md) rule to create a Harper Fabric account, organization, and cluster.
|
|
412
909
|
2. **Configure Environment**: Add your cluster credentials and cluster application URL to `.env`:
|
|
413
910
|
```bash
|
|
414
911
|
CLI_TARGET_USERNAME='YOUR_CLUSTER_USERNAME'
|
|
@@ -422,7 +919,7 @@ Globally scaling your Harper application.
|
|
|
422
919
|
|
|
423
920
|
If your application was not created with `npm create harper`, you'll need to manually configure the deployment scripts and CI/CD workflow.
|
|
424
921
|
|
|
425
|
-
|
|
922
|
+
##### 1. Update `package.json`
|
|
426
923
|
|
|
427
924
|
Add the following scripts and dependencies to your `package.json`:
|
|
428
925
|
|
|
@@ -439,7 +936,7 @@ Add the following scripts and dependencies to your `package.json`:
|
|
|
439
936
|
}
|
|
440
937
|
```
|
|
441
938
|
|
|
442
|
-
|
|
939
|
+
###### Why split the scripts?
|
|
443
940
|
|
|
444
941
|
The `deploy` script is separated from `deploy:component` to ensure environment variables from your `.env` file are properly loaded and passed to the Harper CLI.
|
|
445
942
|
|
|
@@ -448,7 +945,7 @@ The `deploy` script is separated from `deploy:component` to ensure environment v
|
|
|
448
945
|
|
|
449
946
|
By using `dotenv -- npm run deploy:component`, the environment variables are correctly set in the shell session before `harper deploy_component` is called, allowing it to authenticate with your cluster.
|
|
450
947
|
|
|
451
|
-
|
|
948
|
+
##### 2. Configure GitHub Actions
|
|
452
949
|
|
|
453
950
|
Create a `.github/workflows/deploy.yaml` file with the following content:
|
|
454
951
|
|
|
@@ -467,12 +964,15 @@ jobs:
|
|
|
467
964
|
runs-on: ubuntu-latest
|
|
468
965
|
steps:
|
|
469
966
|
- name: Checkout code
|
|
470
|
-
uses: actions/checkout@
|
|
967
|
+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
968
|
+
with:
|
|
969
|
+
fetch-depth: 0
|
|
970
|
+
fetch-tags: true
|
|
471
971
|
- name: Set up Node.js
|
|
472
|
-
uses: actions/setup-node@
|
|
972
|
+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
|
473
973
|
with:
|
|
474
974
|
cache: 'npm'
|
|
475
|
-
node-version: '
|
|
975
|
+
node-version-file: '.nvmrc'
|
|
476
976
|
- name: Install dependencies
|
|
477
977
|
run: npm ci
|
|
478
978
|
- name: Run unit tests
|
|
@@ -487,20 +987,140 @@ jobs:
|
|
|
487
987
|
CLI_TARGET_PASSWORD: ${{ secrets.CLI_TARGET_PASSWORD }}
|
|
488
988
|
```
|
|
489
989
|
|
|
490
|
-
Be sure to set the following repository secrets in your GitHub repository:
|
|
990
|
+
Be sure to set the following repository secrets in your GitHub repository's /settings/secrets/actions:
|
|
491
991
|
|
|
492
992
|
- `CLI_TARGET`
|
|
493
993
|
- `CLI_TARGET_USERNAME`
|
|
494
994
|
- `CLI_TARGET_PASSWORD`
|
|
495
995
|
|
|
996
|
+
### 4.2 Creating a Harper Fabric Account and Cluster
|
|
997
|
+
|
|
998
|
+
Follow these steps to set up your Harper Fabric environment for deployment.
|
|
999
|
+
|
|
1000
|
+
#### How It Works
|
|
1001
|
+
|
|
1002
|
+
1. **Sign Up/In**: Go to [https://fabric.harper.fast/](https://fabric.harper.fast/) and sign up or sign in.
|
|
1003
|
+
2. **Create an Organization**: Create an organization (org) to manage your projects.
|
|
1004
|
+
3. **Create a Cluster**: Create a new cluster. This can be on the free tier, no credit card required.
|
|
1005
|
+
4. **Set Credentials**: During setup, set the cluster username and password to finish configuring it.
|
|
1006
|
+
5. **Get Application URL**: Navigate to the **Config** tab and copy the **Application URL**.
|
|
1007
|
+
6. **Configure Environment**: Update your `.env` file or GitHub Actions secrets with cluster-specific credentials.
|
|
1008
|
+
7. **Next Steps**: See the [deploying-to-harper-fabric](deploying-to-harper-fabric.md) rule for detailed instructions on deploying your application successfully.
|
|
1009
|
+
|
|
1010
|
+
#### Examples
|
|
1011
|
+
|
|
1012
|
+
##### Environment Configuration
|
|
1013
|
+
|
|
1014
|
+
```bash
|
|
1015
|
+
CLI_TARGET_USERNAME='YOUR_CLUSTER_USERNAME'
|
|
1016
|
+
CLI_TARGET_PASSWORD='YOUR_CLUSTER_PASSWORD'
|
|
1017
|
+
CLI_TARGET='YOUR_CLUSTER_URL'
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### 4.3 Creating Harper Applications
|
|
1021
|
+
|
|
1022
|
+
The fastest way to start a new Harper project is using the `create-harper` CLI tool. This command initializes a project with a standard folder structure, essential configuration files, and basic schema definitions.
|
|
1023
|
+
|
|
1024
|
+
#### When to Use
|
|
1025
|
+
|
|
1026
|
+
Use this command when starting a new Harper application or adding a new Harper microservice to an existing architecture.
|
|
1027
|
+
|
|
1028
|
+
#### Commands
|
|
1029
|
+
|
|
1030
|
+
Initialize a project using your preferred package manager:
|
|
1031
|
+
|
|
1032
|
+
##### NPM
|
|
1033
|
+
|
|
1034
|
+
```bash
|
|
1035
|
+
npm create harper@latest
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
##### PNPM
|
|
1039
|
+
|
|
1040
|
+
```bash
|
|
1041
|
+
pnpm create harper@latest
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
##### Bun
|
|
1045
|
+
|
|
1046
|
+
```bash
|
|
1047
|
+
bun create harper@latest
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
#### Options
|
|
1051
|
+
|
|
1052
|
+
You can specify the project name and template directly:
|
|
1053
|
+
|
|
1054
|
+
```bash
|
|
1055
|
+
npm create harper@latest my-app --template default
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
#### Next Steps
|
|
1059
|
+
|
|
1060
|
+
1. **Configure Environment**: Set up your `.env` file with local or cloud credentials.
|
|
1061
|
+
2. **Define Schema**: Modify `schema.graphql` to fit your application's data model.
|
|
1062
|
+
3. **Start Development**: Run `npm run dev` to start the local Harper instance.
|
|
1063
|
+
4. **Deploy**: Use `npm run deploy` to push your application to Harper Fabric.
|
|
1064
|
+
|
|
496
1065
|
### 4.4 Serving Web Content
|
|
497
1066
|
|
|
498
|
-
|
|
1067
|
+
Instructions for the agent to follow when serving web content from Harper.
|
|
1068
|
+
|
|
1069
|
+
#### When to Use
|
|
499
1070
|
|
|
500
|
-
|
|
1071
|
+
Use this skill when you need to serve a frontend (HTML, CSS, JS, or a React app) directly from your Harper instance.
|
|
501
1072
|
|
|
502
|
-
|
|
503
|
-
|
|
1073
|
+
#### How It Works
|
|
1074
|
+
|
|
1075
|
+
1. **Choose a Method**: Decide between the simple Static Plugin or the integrated Vite Plugin.
|
|
1076
|
+
2. **Option A: Static Plugin (Simple)**:
|
|
1077
|
+
- Add to `config.yaml`:
|
|
1078
|
+
```yaml
|
|
1079
|
+
static:
|
|
1080
|
+
files: 'web/*'
|
|
1081
|
+
```
|
|
1082
|
+
- Place files in a `web/` folder in the project root.
|
|
1083
|
+
- Files are served at the root URL (e.g., `http://localhost:9926/index.html`).
|
|
1084
|
+
3. **Option B: Vite Plugin (Advanced/Development)**:
|
|
1085
|
+
- Add to `config.yaml`:
|
|
1086
|
+
```yaml
|
|
1087
|
+
'@harperfast/vite-plugin':
|
|
1088
|
+
package: '@harperfast/vite-plugin'
|
|
1089
|
+
```
|
|
1090
|
+
- Ensure `vite.config.ts` and `index.html` are in the project root.
|
|
1091
|
+
|
|
1092
|
+
```javascript
|
|
1093
|
+
import vue from '@vitejs/plugin-vue';
|
|
1094
|
+
import path from 'node:path';
|
|
1095
|
+
import { defineConfig } from 'vite';
|
|
1096
|
+
|
|
1097
|
+
// https://vite.dev/config/
|
|
1098
|
+
export default defineConfig({
|
|
1099
|
+
plugins: [vue()],
|
|
1100
|
+
resolve: {
|
|
1101
|
+
alias: {
|
|
1102
|
+
'@': path.resolve(import.meta.dirname, './src'),
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
build: {
|
|
1106
|
+
outDir: 'web',
|
|
1107
|
+
emptyOutDir: true,
|
|
1108
|
+
rolldownOptions: {
|
|
1109
|
+
external: ['**/*.test.*', '**/*.spec.*'],
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
- Install dependencies: `npm install --save-dev vite @harperfast/vite-plugin`.
|
|
1116
|
+
- Then `harper run .` will start up Harper and Vite with HMR. Vite does _not_ need to be executed separately.
|
|
1117
|
+
|
|
1118
|
+
4. **Deploy for Production**: For Vite apps, use a build script to generate static files into a `web/` folder and deploy them using the static handler pattern. For example, these scripts in a package.json can perform the necessary steps:
|
|
1119
|
+
```json
|
|
1120
|
+
"build": "vite build",
|
|
1121
|
+
"deploy": "rm -Rf deploy && npm run build && mkdir deploy && mv web deploy/ && cp -R deploy-template/* deploy/ && cp -R schemas resources deploy/ && (cd deploy && harper deploy_component . project=web restart=rolling replicated=true) && rm -Rf deploy",
|
|
1122
|
+
```
|
|
1123
|
+
Then in production, the "Static Plugin" option will performantly and securely serve your assets. `npm create harper@latest` scaffolds all of this for you.
|
|
504
1124
|
|
|
505
1125
|
### 4.5 Logging Best Practices
|
|
506
1126
|
|