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