@harperfast/template-react-ts-studio 1.12.2 → 1.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/harper-best-practices/AGENTS.md +778 -580
- package/.agents/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/.agents/skills/harper-best-practices/rules/automatic-apis.md +131 -55
- package/.agents/skills/harper-best-practices/rules/checking-authentication.md +265 -127
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +63 -85
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +125 -52
- package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +38 -70
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +81 -69
- package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +82 -96
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +273 -2
- package/.agents/skills/harper-mcp/AGENTS.md +77 -31
- package/.agents/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/.agents/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/agent/skills/harper-best-practices/AGENTS.md +778 -580
- package/agent/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/agent/skills/harper-best-practices/rules/automatic-apis.md +131 -55
- package/agent/skills/harper-best-practices/rules/checking-authentication.md +265 -127
- package/agent/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +63 -85
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +125 -52
- package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +38 -70
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +81 -69
- package/agent/skills/harper-best-practices/rules/v5-upgrade.md +82 -96
- package/agent/skills/harper-best-practices/rules.manifest.yaml +273 -2
- package/agent/skills/harper-mcp/AGENTS.md +77 -31
- package/agent/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/agent/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/package.json +1 -1
- package/skills-lock.json +2 -2
|
@@ -18,14 +18,14 @@ Use this skill when you need to define new data structures or modify existing on
|
|
|
18
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`.
|
|
19
19
|
3. **Define Relationships**: Link tables together using the `@relationship` directive. For more details, see the [Defining Relationships](defining-relationships.md) skill.
|
|
20
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
|
|
21
|
+
- `GET /{TableName}`: Describes the resource itself — table, database, and declared attributes. No trailing slash.
|
|
22
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
|
|
24
|
-
- `POST /{TableName}/`: Creates a
|
|
25
|
-
- `PUT /{TableName}/{id}`:
|
|
26
|
-
- `PATCH /{TableName}/{id}`:
|
|
27
|
-
- `DELETE /{TableName}
|
|
28
|
-
- `DELETE /{TableName}
|
|
23
|
+
- `GET /{TableName}/{id}`: Retrieves a single record by its primary key.
|
|
24
|
+
- `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`.
|
|
25
|
+
- `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). **Properties omitted from the body are removed** — send the complete record, or use `PATCH` to change a subset. Three exceptions survive the replacement: an `@updatedTime` attribute is re-stamped with the time of the write, a `@createdTime` attribute keeps its original value, and the primary key is forced to match the `{id}` in the URL, so a mismatched key in the body cannot create a second record.
|
|
26
|
+
- `PATCH /{TableName}/{id}`: Merges the request body into the existing record, preserving unspecified properties. The merge is **shallow** — a nested object in the body replaces the stored one wholesale rather than being deep-merged.
|
|
27
|
+
- `DELETE /{TableName}/{id}`: Deletes a single record by its primary key.
|
|
28
|
+
- `DELETE /{TableName}/`: Deletes every record matching the query parameters. **With no query parameters it matches — and deletes — every record in the table.** Always pass a filter unless emptying the table is the intent.
|
|
29
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
30
|
|
|
31
31
|
#### Examples
|
|
@@ -42,15 +42,26 @@ type ExamplePerson @table @export {
|
|
|
42
42
|
|
|
43
43
|
### 1.2 Schema Design and GraphQL Tooling
|
|
44
44
|
|
|
45
|
-
Instructions for the agent to follow when designing Harper schemas,
|
|
45
|
+
Instructions for the agent to follow when designing Harper database schemas using GraphQL type definitions, core directives, and tooling configuration.
|
|
46
46
|
|
|
47
47
|
#### When to Use
|
|
48
48
|
|
|
49
|
-
Apply this rule when creating or modifying Harper schema files (`.graphql`), configuring
|
|
49
|
+
Apply this rule when creating or modifying Harper schema files (`.graphql`), configuring schema loading in `config.yaml`, or deciding which directives to apply to tables and fields. Use it whenever a task involves defining tables, primary keys, indexes, or export behavior.
|
|
50
50
|
|
|
51
51
|
#### How It Works
|
|
52
52
|
|
|
53
|
-
1. **
|
|
53
|
+
1. **Create a GraphQL schema file** with Harper-specific directives. Schemas ensure required tables exist on deployment, enforce types and constraints, control indexing, and define relationships.
|
|
54
|
+
|
|
55
|
+
```graphql
|
|
56
|
+
type Dog @table {
|
|
57
|
+
id: Long @primaryKey
|
|
58
|
+
name: String
|
|
59
|
+
breed: String
|
|
60
|
+
age: Int
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
2. **Register the schema in `config.yaml`** using the `graphqlSchema` plugin:
|
|
54
65
|
|
|
55
66
|
```yaml
|
|
56
67
|
graphqlSchema:
|
|
@@ -59,76 +70,74 @@ Apply this rule when creating or modifying Harper schema files (`.graphql`), con
|
|
|
59
70
|
|
|
60
71
|
Both plugins and applications can specify schemas.
|
|
61
72
|
|
|
62
|
-
|
|
73
|
+
3. **Mark each type as a table** with `@table`. The type name becomes the table name by default.
|
|
63
74
|
|
|
64
75
|
```graphql
|
|
65
|
-
type
|
|
76
|
+
type MyTable @table {
|
|
66
77
|
id: Long @primaryKey
|
|
67
|
-
name: String
|
|
68
|
-
breed: String
|
|
69
|
-
age: Int
|
|
70
78
|
}
|
|
71
79
|
```
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
- `String` or `ID` → UUID string
|
|
75
|
-
- `Int`, `Long`, or `Any` → auto-incrementing integer
|
|
81
|
+
Key `@table` arguments:
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
| Argument | Type | Default | Description |
|
|
84
|
+
| -------------------- | --------- | ----------------------------- | --------------------------------------------------------------- |
|
|
85
|
+
| `table` | `String` | type name | Override the table name |
|
|
86
|
+
| `database` | `String` | `"data"` | Database to place the table in |
|
|
87
|
+
| `expiration` | `Int` | — | Seconds until a record goes stale |
|
|
88
|
+
| `eviction` | `Int` | `0` | Additional seconds after `expiration` before physical removal |
|
|
89
|
+
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
|
|
90
|
+
| `replicate` | `Boolean` | `true` | Enable replication of this table |
|
|
91
|
+
| `cacheControl` | `String` | — | `Cache-Control` header for anonymous GET/HEAD 200/304 responses |
|
|
92
|
+
| `randomAccessFields` | `Boolean` | `storage.randomAccessFields` | Pin this table's record encoding |
|
|
78
93
|
|
|
79
|
-
4. **
|
|
94
|
+
4. **Designate a primary key** on every table using `@primaryKey`. Primary keys must be unique; duplicate inserts are rejected. If no primary key is provided on insert, Harper auto-generates one:
|
|
95
|
+
- **UUID string** — when type is `String` or `ID`
|
|
96
|
+
- **Auto-incrementing integer** — when type is `Int`, `Long`, or `Any`
|
|
97
|
+
|
|
98
|
+
Use `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
|
|
80
99
|
|
|
81
100
|
```graphql
|
|
82
|
-
type
|
|
101
|
+
type Product @table {
|
|
83
102
|
id: Long @primaryKey
|
|
84
|
-
name: String
|
|
103
|
+
name: String
|
|
85
104
|
}
|
|
86
105
|
```
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
5. **Expose a table as an external endpoint** with `@export`. Available via REST, MQTT, and other interfaces. The optional `name` parameter sets the URL path segment:
|
|
107
|
+
5. **Add secondary indexes** with `@indexed` on any attribute that will be used for filtering in REST queries, SQL, or NoSQL operations. If the field value is an array, each element is individually indexed.
|
|
91
108
|
|
|
92
109
|
```graphql
|
|
93
|
-
type
|
|
110
|
+
type Product @table {
|
|
94
111
|
id: Long @primaryKey
|
|
112
|
+
category: String @indexed
|
|
113
|
+
price: Float @indexed
|
|
95
114
|
}
|
|
96
115
|
```
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
6. **Configure `@table` arguments** as needed. All arguments are optional:
|
|
101
|
-
|
|
102
|
-
| Argument | Type | Default | Description |
|
|
103
|
-
| -------------- | --------- | ----------------------------- | ------------------------------------------------------------- |
|
|
104
|
-
| `table` | `String` | type name | Override the table name |
|
|
105
|
-
| `database` | `String` | `"data"` | Database to place the table in |
|
|
106
|
-
| `expiration` | `Int` | — | Seconds until a record goes stale |
|
|
107
|
-
| `eviction` | `Int` | `0` | Additional seconds after `expiration` before physical removal |
|
|
108
|
-
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
|
|
109
|
-
| `replicate` | `Boolean` | `true` | Enable replication of this table |
|
|
110
|
-
|
|
111
|
-
7. **Apply additional field directives** where needed:
|
|
112
|
-
- `@createdTime` — auto-assigns creation timestamp (Unix epoch ms)
|
|
113
|
-
- `@updatedTime` — auto-assigns update timestamp (Unix epoch ms)
|
|
114
|
-
- `@expiresAt` — marks a field as the record's absolute expiration time (Unix epoch ms)
|
|
115
|
-
- `@embed(source:, model:)` — computes an embedding vector when the source field is written; field type must be `[Float]`
|
|
116
|
-
- `@hidden` — suppresses the field from MCP tool descriptors and OpenAPI document (not an access-control mechanism)
|
|
117
|
-
|
|
118
|
-
8. **Use `@sealed`** on a type to prevent records from including properties beyond those declared in the schema:
|
|
117
|
+
6. **Expose tables via REST and other interfaces** using `@export`. Without `@export`, the table has no REST/MQTT route (callers get 404). The optional `name` parameter sets the URL path segment.
|
|
119
118
|
|
|
120
119
|
```graphql
|
|
121
|
-
type
|
|
120
|
+
type MyTable @table @export(name: "my-table") {
|
|
122
121
|
id: Long @primaryKey
|
|
123
|
-
name: String
|
|
124
122
|
}
|
|
125
123
|
```
|
|
126
124
|
|
|
127
|
-
|
|
125
|
+
`@export` is a routing directive, not access control. The table remains accessible through the Operations API and SQL regardless. REST must also be enabled for the application (via `rest: true` in `config.yaml` or Harper's built-in default).
|
|
126
|
+
|
|
127
|
+
7. **Apply additional type directives** as needed:
|
|
128
|
+
- `@sealed` — prevents records from including properties beyond those declared in the schema.
|
|
129
|
+
- `@hidden` — suppresses the type from MCP tool descriptors and the OpenAPI document. Does not restrict data access.
|
|
130
|
+
|
|
131
|
+
8. **Apply field directives** for computed and lifecycle behavior:
|
|
132
|
+
- `@createdTime` — assigns Unix epoch milliseconds on record creation.
|
|
133
|
+
- `@updatedTime` — assigns Unix epoch milliseconds on each update.
|
|
134
|
+
- `@expiresAt` — marks a field as the record's absolute expiration time (Unix epoch milliseconds); authoritative over the table-level `expiration` default.
|
|
135
|
+
- `@embed` — computes an embedding vector when the source field is written (requires `source` and `model` arguments; field type must be `[Float]`).
|
|
136
|
+
- `@hidden` (field) — suppresses the field from generated specs and MCP tool schemas; does not restrict data access.
|
|
128
137
|
|
|
129
138
|
#### Examples
|
|
130
139
|
|
|
131
|
-
**Minimal
|
|
140
|
+
**Minimal two-table schema:**
|
|
132
141
|
|
|
133
142
|
```graphql
|
|
134
143
|
type Dog @table {
|
|
@@ -154,59 +163,62 @@ type WeatherCache @table(expiration: 300, eviction: 3300, scanInterval: 600) {
|
|
|
154
163
|
}
|
|
155
164
|
```
|
|
156
165
|
|
|
157
|
-
**
|
|
166
|
+
**Exported table with cache control:**
|
|
158
167
|
|
|
159
168
|
```graphql
|
|
160
|
-
type
|
|
169
|
+
type Product @table(cacheControl: "public, max-age=60") @export {
|
|
161
170
|
id: Long @primaryKey
|
|
162
|
-
name: String
|
|
171
|
+
name: String
|
|
172
|
+
price: Float
|
|
163
173
|
}
|
|
164
174
|
```
|
|
165
175
|
|
|
166
|
-
**
|
|
176
|
+
**Table with lifecycle fields and indexing:**
|
|
167
177
|
|
|
168
178
|
```graphql
|
|
169
|
-
type
|
|
179
|
+
type Event @table(database: "analytics", expiration: 86400) {
|
|
170
180
|
id: Long @primaryKey
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
name: String @indexed
|
|
182
|
+
createdAt: Long @createdTime
|
|
183
|
+
updatedAt: Long @updatedTime
|
|
173
184
|
}
|
|
174
185
|
```
|
|
175
186
|
|
|
176
|
-
**
|
|
187
|
+
**Session table with per-record expiration:**
|
|
177
188
|
|
|
178
189
|
```graphql
|
|
179
190
|
type Session @table {
|
|
180
191
|
id: ID @primaryKey
|
|
181
192
|
token: String
|
|
182
|
-
createdAt: Long @createdTime
|
|
183
|
-
updatedAt: Long @updatedTime
|
|
184
193
|
expiresAt: Long @expiresAt
|
|
185
194
|
}
|
|
186
195
|
```
|
|
187
196
|
|
|
188
|
-
**
|
|
197
|
+
**Sealed table preventing extra properties:**
|
|
189
198
|
|
|
190
199
|
```graphql
|
|
191
|
-
type
|
|
200
|
+
type StrictRecord @table @sealed {
|
|
192
201
|
id: Long @primaryKey
|
|
193
202
|
name: String
|
|
194
|
-
|
|
195
|
-
"""
|
|
196
|
-
Internal — do not surface to external consumers.
|
|
197
|
-
"""
|
|
198
|
-
creditScore: Int @hidden
|
|
199
203
|
}
|
|
200
204
|
```
|
|
201
205
|
|
|
206
|
+
**`config.yaml` schema registration:**
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
graphqlSchema:
|
|
210
|
+
files: 'schema.graphql'
|
|
211
|
+
```
|
|
212
|
+
|
|
202
213
|
#### Notes
|
|
203
214
|
|
|
204
|
-
-
|
|
205
|
-
- `
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
- `@
|
|
209
|
-
-
|
|
215
|
+
- Schemas are flexible by default — records may include additional properties beyond those declared. Use `@sealed` to prevent this.
|
|
216
|
+
- Use unique `database` names in plugins or applications to avoid table naming collisions, since all tables default to the `"data"` database.
|
|
217
|
+
- Replication is enabled by default. If you disable replication and re-enable it later, the table will not catch up on writes made while replication was disabled.
|
|
218
|
+
- `@hidden` (type or field) is a metadata-visibility directive only. Use table-level role permissions and `attribute_permissions` whitelists to restrict actual data access.
|
|
219
|
+
- `@export` absence causes 404 on REST/MQTT routes but does not protect data from the Operations API or SQL.
|
|
220
|
+
- The `cacheControl` argument emits headers only on anonymous (unauthenticated) GET/HEAD 200/304 responses. Authenticated responses receive `Cache-Control: private, no-cache`.
|
|
221
|
+
- `randomAccessFields` on `@table` pins the record encoding at table creation time. Editing the argument later does not repin an existing table.
|
|
210
222
|
|
|
211
223
|
### 1.3 Defining Relationships Between Tables in Harper
|
|
212
224
|
|
|
@@ -754,15 +766,15 @@ export class Photo extends tables.Photo {
|
|
|
754
766
|
|
|
755
767
|
### 2.1 Automatic APIs
|
|
756
768
|
|
|
757
|
-
Instructions for the agent to follow when
|
|
769
|
+
Instructions for the agent to follow when using Harper's automatically generated REST and WebSocket APIs for exported tables and resources.
|
|
758
770
|
|
|
759
771
|
#### When to Use
|
|
760
772
|
|
|
761
|
-
Apply this rule when
|
|
773
|
+
Apply this rule when enabling HTTP REST endpoints or WebSocket subscriptions for Harper tables without writing custom handler code. Use it whenever a schema type needs to be served over HTTP, when configuring real-time subscriptions, or when setting up conditional caching behavior for REST responses.
|
|
762
774
|
|
|
763
775
|
#### How It Works
|
|
764
776
|
|
|
765
|
-
1. **Enable
|
|
777
|
+
1. **Enable REST in `config.yaml`**: Add `rest: true` to the application configuration file. This registers REST endpoints and, by default, WebSocket subscriptions for all exported resources.
|
|
766
778
|
|
|
767
779
|
```yaml
|
|
768
780
|
rest: true
|
|
@@ -776,26 +788,47 @@ Apply this rule when adding REST or WebSocket API access to Harper tables or cus
|
|
|
776
788
|
webSocket: false # disables automatic WebSocket support (enabled by default)
|
|
777
789
|
```
|
|
778
790
|
|
|
779
|
-
2. **Export
|
|
791
|
+
2. **Export the table in the schema**: Add `@export` to the type definition. Without `@export`, Harper registers no REST route and callers receive `404`. Without `rest: true`, even an exported table does not respond to HTTP requests. Both are required.
|
|
780
792
|
|
|
781
|
-
|
|
793
|
+
```graphql
|
|
794
|
+
type Product @table @export {
|
|
795
|
+
id: Long @primaryKey
|
|
796
|
+
name: String
|
|
797
|
+
price: Float
|
|
798
|
+
}
|
|
799
|
+
```
|
|
782
800
|
|
|
783
|
-
|
|
784
|
-
| -------------------------------------------- | ----------------------------------------------------------- |
|
|
785
|
-
| `/my-resource` | Returns resource metadata |
|
|
786
|
-
| `/my-resource/` | Collection — all records; append query parameters to search |
|
|
787
|
-
| `/my-resource/record-id` | Specific record by primary key |
|
|
788
|
-
| `/my-resource/record-id/` | Collection of records with the given id prefix |
|
|
789
|
-
| `/my-resource/record-id/with/multiple/parts` | Record id with multiple path segments |
|
|
801
|
+
Reference the schema file in `config.yaml`:
|
|
790
802
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
803
|
+
```yaml
|
|
804
|
+
graphqlSchema:
|
|
805
|
+
files: schema.graphql
|
|
806
|
+
rest: true
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
3. **Use the automatically registered endpoints**: Harper serves the following endpoints on the application HTTP server port (default `9926`) with no route definitions or handler code required.
|
|
810
|
+
|
|
811
|
+
| Endpoint | Description |
|
|
812
|
+
| ---------------------------- | --------------------------------------------------------------------------- |
|
|
813
|
+
| `GET /Product` | Returns resource description (table name, database, attributes) |
|
|
814
|
+
| `GET /Product/` | Returns the record collection; append query parameters to filter |
|
|
815
|
+
| `GET /Product/{id}` | Returns a single record by primary key; `404` if not found |
|
|
816
|
+
| `GET /Product/{id}.property` | Returns a single declared property of one record |
|
|
817
|
+
| `POST /Product/` | Creates a record; responds `201`; primary key returned in `Location` header |
|
|
818
|
+
| `PUT /Product/{id}` | Creates or replaces the record at `{id}` (upsert) |
|
|
819
|
+
| `PATCH /Product/{id}` | Merges body into existing record (shallow, top-level only) |
|
|
820
|
+
| `DELETE /Product/{id}` | Deletes the record at `{id}` |
|
|
821
|
+
| `DELETE /Product/?query` | Deletes every record matching the query |
|
|
822
|
+
|
|
823
|
+
4. **Handle `POST` primary key and `Location`**: On a successful `POST`, the new record's primary key is returned in the `Location` response header — the value the body supplied if it carried the primary-key property, otherwise a Harper-assigned key. The header carries the bare key value, not a URL.
|
|
797
824
|
|
|
798
|
-
5. **
|
|
825
|
+
5. **Understand `PUT` write behavior**: `PUT` replaces the stored record exactly. Three exceptions always apply: a `@createdTime` attribute keeps the original value, an `@updatedTime` attribute is re-stamped with the time of the write, and the primary key is forced to match the `{id}` in the URL.
|
|
826
|
+
|
|
827
|
+
6. **Use conditional requests for caching**: GET responses include an `ETag` header encoding the record's version/last-modification time. Send `If-None-Match` on subsequent requests with the cached `ETag` value. If the record has not changed, Harper returns `304 Not Modified` with no body.
|
|
828
|
+
|
|
829
|
+
7. **Select content type with `Accept`**: Use the `Accept` header to request a specific response format. The suffixes `.json`, `.cbor`, `.msgpack`, and `.csv` are reserved as content-type selectors on property paths and take precedence over property names. See [querying-rest-apis.md](querying-rest-apis.md) for query syntax details.
|
|
830
|
+
|
|
831
|
+
8. **Connect via WebSocket**: WebSocket support is enabled automatically when `rest` is enabled. Connecting to a resource URL subscribes to changes for that resource. See [real-time-apps.md](real-time-apps.md) for real-time patterns.
|
|
799
832
|
|
|
800
833
|
```javascript
|
|
801
834
|
let ws = new WebSocket('wss://server/my-resource/341');
|
|
@@ -804,29 +837,84 @@ Apply this rule when adding REST or WebSocket API access to Harper tables or cus
|
|
|
804
837
|
};
|
|
805
838
|
```
|
|
806
839
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
7. **Retrieve the OpenAPI spec**: Harper auto-generates an OpenAPI specification for all exported resources, available at:
|
|
840
|
+
9. **Implement a custom `connect()` handler** when default subscription behavior is insufficient. The method must return an async iterable that produces messages to send to the client.
|
|
810
841
|
|
|
811
|
-
```
|
|
812
|
-
|
|
842
|
+
```javascript
|
|
843
|
+
export class Echo extends Resource {
|
|
844
|
+
async *connect(incomingMessages) {
|
|
845
|
+
for await (let message of incomingMessages) {
|
|
846
|
+
yield message; // echo each message back
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
813
850
|
```
|
|
814
851
|
|
|
815
852
|
#### Examples
|
|
816
853
|
|
|
817
|
-
|
|
854
|
+
##### Full schema and config setup
|
|
818
855
|
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
856
|
+
```graphql
|
|
857
|
+
# schema.graphql
|
|
858
|
+
type Product @table @export {
|
|
859
|
+
id: Long @primaryKey
|
|
860
|
+
name: String
|
|
861
|
+
price: Float
|
|
826
862
|
}
|
|
827
863
|
```
|
|
828
864
|
|
|
829
|
-
|
|
865
|
+
```yaml
|
|
866
|
+
# config.yaml
|
|
867
|
+
graphqlSchema:
|
|
868
|
+
files: schema.graphql
|
|
869
|
+
rest: true
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
##### Conditional GET with ETag caching
|
|
873
|
+
|
|
874
|
+
```
|
|
875
|
+
GET /Product/123
|
|
876
|
+
# Response includes:
|
|
877
|
+
# ETag: "abc123"
|
|
878
|
+
|
|
879
|
+
GET /Product/123
|
|
880
|
+
If-None-Match: "abc123"
|
|
881
|
+
# Response: 304 Not Modified (no body transferred)
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
##### POST and read the Location header
|
|
885
|
+
|
|
886
|
+
```
|
|
887
|
+
POST /Product/
|
|
888
|
+
Content-Type: application/json
|
|
889
|
+
|
|
890
|
+
{ "name": "Widget", "price": 9.99 }
|
|
891
|
+
|
|
892
|
+
# Response:
|
|
893
|
+
# 201 Created
|
|
894
|
+
# Location: 7f3a9c
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
##### PATCH (shallow merge only)
|
|
898
|
+
|
|
899
|
+
```
|
|
900
|
+
PATCH /Product/123
|
|
901
|
+
Content-Type: application/json
|
|
902
|
+
|
|
903
|
+
{ "price": 12.99 }
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Only `price` is updated; other top-level properties are preserved. Nested objects in the body replace the stored sub-object wholesale — deep merge does not occur.
|
|
907
|
+
|
|
908
|
+
##### Request MessagePack response
|
|
909
|
+
|
|
910
|
+
```
|
|
911
|
+
GET /Product/123
|
|
912
|
+
Accept: application/msgpack
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
Alternatively, use the `.msgpack` suffix on the URL path where supported.
|
|
916
|
+
|
|
917
|
+
##### WebSocket with custom outgoing messages
|
|
830
918
|
|
|
831
919
|
```javascript
|
|
832
920
|
export class Example extends Resource {
|
|
@@ -838,7 +926,7 @@ export class Example extends Resource {
|
|
|
838
926
|
}, 1000);
|
|
839
927
|
|
|
840
928
|
incomingMessages.on('data', (message) => {
|
|
841
|
-
outgoingMessages.send(message);
|
|
929
|
+
outgoingMessages.send(message);
|
|
842
930
|
});
|
|
843
931
|
|
|
844
932
|
outgoingMessages.on('close', () => {
|
|
@@ -850,37 +938,31 @@ export class Example extends Resource {
|
|
|
850
938
|
}
|
|
851
939
|
```
|
|
852
940
|
|
|
853
|
-
|
|
941
|
+
##### Disable WebSocket while keeping REST
|
|
854
942
|
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
PUT /MyTable/123
|
|
859
|
-
PATCH /MyTable/123
|
|
860
|
-
DELETE /MyTable/?status=archived
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
```json
|
|
864
|
-
{ "name": "some data" }
|
|
943
|
+
```yaml
|
|
944
|
+
rest:
|
|
945
|
+
webSocket: false
|
|
865
946
|
```
|
|
866
947
|
|
|
867
948
|
#### Notes
|
|
868
949
|
|
|
869
|
-
-
|
|
870
|
-
-
|
|
871
|
-
-
|
|
872
|
-
-
|
|
873
|
-
-
|
|
874
|
-
-
|
|
875
|
-
-
|
|
950
|
+
- The trailing slash is significant: `/Product` addresses the resource itself; `/Product/` addresses its record collection. `POST /Product` (no trailing slash) returns `404`.
|
|
951
|
+
- `HEAD` is served as `GET` with the body omitted. `QUERY` is accepted on the collection path and reads its search from the request body.
|
|
952
|
+
- A `POST` to an existing primary key fails with `409` — it does not overwrite.
|
|
953
|
+
- A component directory with **no configuration file** gets REST enabled by Harper's built-in default. As soon as a `config.yaml` exists it is used verbatim — add `rest: true` explicitly or REST is off.
|
|
954
|
+
- Do not apply `@export` to a schema type and also export a same-named JavaScript subclass of that table — this produces conflicting endpoints.
|
|
955
|
+
- Server-Sent Events subscriptions are served on the same paths, negotiated via `Accept: text/event-stream`. They are not affected by the `webSocket` option.
|
|
956
|
+
- Every non-hidden exported resource is included in the generated OpenAPI document at `GET /openapi`. Mark a type `@hidden` or set `static hidden = true` on a programmatic Resource to omit it.
|
|
957
|
+
- MQTT over WebSockets requires the sub-protocol header `Sec-WebSocket-Protocol: mqtt`.
|
|
876
958
|
|
|
877
|
-
### 2.2 Querying
|
|
959
|
+
### 2.2 Querying REST APIs
|
|
878
960
|
|
|
879
|
-
Instructions for the agent to filter, sort, select, and paginate
|
|
961
|
+
Instructions for the agent to filter, sort, select, and paginate Harper REST API collections using URL query parameters.
|
|
880
962
|
|
|
881
963
|
#### When to Use
|
|
882
964
|
|
|
883
|
-
Apply this rule
|
|
965
|
+
Apply this rule whenever building or modifying code that queries Harper REST collection endpoints. Use it when you need to filter records by attribute values, apply comparison operators, sort or paginate results, or join across related tables. See [automatic-apis.md](automatic-apis.md) for how Harper exposes tables as REST endpoints.
|
|
884
966
|
|
|
885
967
|
#### How It Works
|
|
886
968
|
|
|
@@ -891,13 +973,7 @@ Apply this rule when building or debugging REST API calls against Harper tables
|
|
|
891
973
|
GET /Product/?category=software&inStock=true
|
|
892
974
|
```
|
|
893
975
|
|
|
894
|
-
2. **
|
|
895
|
-
|
|
896
|
-
```
|
|
897
|
-
GET /Product/?discount=null
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
3. **Apply comparison operators (FIQL syntax)**: Use FIQL operators in query parameters for range and string matching.
|
|
976
|
+
2. **Apply comparison operators (FIQL syntax)**: Use FIQL operators in the query string for numeric, string, and date comparisons.
|
|
901
977
|
|
|
902
978
|
| Operator | Meaning |
|
|
903
979
|
| -------------------- | -------------------------------------- |
|
|
@@ -926,13 +1002,13 @@ Apply this rule when building or debugging REST API calls against Harper tables
|
|
|
926
1002
|
GET /Product/?listDate=gt=2017-03-08T09%3A30%3A00.000Z
|
|
927
1003
|
```
|
|
928
1004
|
|
|
929
|
-
|
|
1005
|
+
3. **Chain conditions for range queries**: Omit the attribute name on the second condition to apply it to the same attribute. Only `gt`/`ge` combined with `lt`/`le` is supported.
|
|
930
1006
|
|
|
931
1007
|
```
|
|
932
1008
|
GET /Product/?price=gt=100<=200
|
|
933
1009
|
```
|
|
934
1010
|
|
|
935
|
-
|
|
1011
|
+
4. **Apply type conversion**: For FIQL comparators, Harper converts values automatically. Use explicit prefixes to force a type.
|
|
936
1012
|
|
|
937
1013
|
| Syntax | Behavior |
|
|
938
1014
|
| ----------------------------------------- | ------------------------------------------- |
|
|
@@ -946,32 +1022,26 @@ Apply this rule when building or debugging REST API calls against Harper tables
|
|
|
946
1022
|
|
|
947
1023
|
For strict operators (`=`, `===`, `!==`), no automatic type conversion is applied.
|
|
948
1024
|
|
|
949
|
-
|
|
1025
|
+
5. **Combine conditions with OR logic**: Use `|` instead of `&`.
|
|
950
1026
|
|
|
951
1027
|
```
|
|
952
1028
|
GET /Product/?rating=5|featured=true
|
|
953
1029
|
```
|
|
954
1030
|
|
|
955
|
-
|
|
1031
|
+
6. **Group conditions**: Use parentheses or square brackets to control order of operations. Prefer square brackets when constructing queries from user input, since standard URI encoding safely encodes `[` and `]`.
|
|
956
1032
|
|
|
957
1033
|
```
|
|
958
1034
|
GET /Product/?rating=5|(price=gt=100&price=lt=200)
|
|
959
1035
|
GET /Product/?rating=5&[tag=fast|tag=scalable|tag=efficient]
|
|
960
1036
|
```
|
|
961
1037
|
|
|
962
|
-
|
|
1038
|
+
Construct from JavaScript:
|
|
963
1039
|
|
|
964
1040
|
```javascript
|
|
965
1041
|
let url = `/Product/?rating=5&[${tags.map(encodeURIComponent).join('|')}]`;
|
|
966
1042
|
```
|
|
967
1043
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
```
|
|
971
|
-
GET /Product/?price=lt=100|[rating=5&[tag=fast|tag=scalable|tag=efficient]&inStock=true]
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
8. **Select specific properties with `select(`**: Append `select(...)` as a query function separated by `&` to control which fields are returned.
|
|
1044
|
+
7. **Select specific properties with `select(`**: Append `select(...)` as a query function separated by `&`.
|
|
975
1045
|
|
|
976
1046
|
| Syntax | Returns |
|
|
977
1047
|
| -------------------------------------- | ------------------------------------------- |
|
|
@@ -981,65 +1051,47 @@ Apply this rule when building or debugging REST API calls against Harper tables
|
|
|
981
1051
|
| `?select(property1,)` | Objects with a single specified property |
|
|
982
1052
|
| `?select(property{subProp1,subProp2})` | Nested objects with specific sub-properties |
|
|
983
1053
|
|
|
984
|
-
|
|
985
|
-
GET /Product/?category=software&select(name)
|
|
986
|
-
GET /Product/?brand.name=Microsoft&select(name,brand{name})
|
|
987
|
-
```
|
|
988
|
-
|
|
989
|
-
9. **Paginate results with `limit(`**: Use `limit(end)` or `limit(start,end)` to restrict the number of records returned.
|
|
990
|
-
|
|
991
|
-
```
|
|
992
|
-
GET /Product/?rating=gt=3&inStock=true&select(rating,name)&limit(20)
|
|
993
|
-
GET /Product/?rating=gt=3&limit(10,30)
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
10. **Sort results with `sort(`**: Use `sort(property)` or `sort(+property,-property,...)` to order results. Prefix `+` or no prefix = ascending; `-` = descending.
|
|
997
|
-
|
|
998
|
-
```
|
|
999
|
-
GET /Product/?rating=gt=3&sort(+name)
|
|
1000
|
-
GET /Product/?sort(+rating,-price)
|
|
1001
|
-
```
|
|
1002
|
-
|
|
1003
|
-
11. **Query across relationships using dot-syntax**: Filter on related table attributes using dot-chained property names. Relationships must be defined in the schema with `@relationship`.
|
|
1054
|
+
8. **Paginate with `limit(`**: Use `limit(end)` or `limit(start,end)` to control result count and offset.
|
|
1004
1055
|
|
|
1005
|
-
|
|
1006
|
-
GET /Product/?brand.name=Microsoft
|
|
1007
|
-
GET /Brand/?products.name=Keyboard
|
|
1008
|
-
```
|
|
1056
|
+
9. **Sort with `sort(`**: Use `sort(property)` or `sort(+property,-property,...)`. Prefix `+` or no prefix = ascending; `-` = descending.
|
|
1009
1057
|
|
|
1010
|
-
|
|
1058
|
+
10. **Query across relationships**: Use dot-syntax to filter by related table attributes. Relationships must be defined in the schema using `@relationship`. Relationship attributes are not included by default — use `select()` to include them.
|
|
1011
1059
|
|
|
1012
1060
|
```
|
|
1013
|
-
GET /Product/?brand.name=Microsoft&select(name,brand)
|
|
1014
1061
|
GET /Product/?brand.name=Microsoft&select(name,brand{name})
|
|
1015
1062
|
```
|
|
1016
1063
|
|
|
1017
|
-
|
|
1064
|
+
11. **Query for null values**: Use `=null` as the value to match null or non-null records.
|
|
1018
1065
|
```
|
|
1019
|
-
GET /
|
|
1066
|
+
GET /Product/?discount=null
|
|
1020
1067
|
```
|
|
1021
1068
|
|
|
1022
1069
|
#### Examples
|
|
1023
1070
|
|
|
1024
|
-
**
|
|
1071
|
+
**Filter with comparison operators and select:**
|
|
1025
1072
|
|
|
1026
1073
|
```
|
|
1027
|
-
GET /Product/?category=software&price=gt=100&price=lt=200&select(name,price)
|
|
1074
|
+
GET /Product/?category=software&price=gt=100&price=lt=200&select(name,price)
|
|
1028
1075
|
```
|
|
1029
1076
|
|
|
1030
|
-
**
|
|
1077
|
+
**Paginate and sort:**
|
|
1031
1078
|
|
|
1032
1079
|
```
|
|
1033
|
-
GET /Product/?rating=gt=3&
|
|
1080
|
+
GET /Product/?rating=gt=3&inStock=true&select(rating,name)&limit(20)
|
|
1081
|
+
GET /Product/?rating=gt=3&limit(10,30)
|
|
1082
|
+
GET /Product/?rating=gt=3&sort(+name)
|
|
1083
|
+
GET /Product/?sort(+rating,-price)
|
|
1034
1084
|
```
|
|
1035
1085
|
|
|
1036
|
-
**OR with grouping
|
|
1086
|
+
**OR logic with grouping:**
|
|
1037
1087
|
|
|
1038
1088
|
```
|
|
1039
1089
|
GET /Product/?price=lt=100|[rating=5&[tag=fast|tag=scalable|tag=efficient]&inStock=true]
|
|
1040
1090
|
```
|
|
1041
1091
|
|
|
1042
|
-
**
|
|
1092
|
+
**Relationship join with nested select:**
|
|
1093
|
+
|
|
1094
|
+
Define the schema:
|
|
1043
1095
|
|
|
1044
1096
|
```graphql
|
|
1045
1097
|
type Product @table @export {
|
|
@@ -1055,13 +1107,14 @@ type Brand @table @export {
|
|
|
1055
1107
|
}
|
|
1056
1108
|
```
|
|
1057
1109
|
|
|
1058
|
-
|
|
1110
|
+
Query with join:
|
|
1059
1111
|
|
|
1060
1112
|
```
|
|
1061
1113
|
GET /Product/?brand.name=Microsoft&select(name,brand{name,id})
|
|
1114
|
+
GET /Brand/?products.name=Keyboard
|
|
1062
1115
|
```
|
|
1063
1116
|
|
|
1064
|
-
**Many-to-many relationship
|
|
1117
|
+
**Many-to-many relationship:**
|
|
1065
1118
|
|
|
1066
1119
|
```graphql
|
|
1067
1120
|
type Product @table @export {
|
|
@@ -1072,26 +1125,23 @@ type Product @table @export {
|
|
|
1072
1125
|
}
|
|
1073
1126
|
```
|
|
1074
1127
|
|
|
1075
|
-
Query:
|
|
1076
|
-
|
|
1077
1128
|
```
|
|
1078
1129
|
GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
|
|
1079
1130
|
```
|
|
1080
1131
|
|
|
1081
|
-
**
|
|
1132
|
+
**Access a specific property by record ID:**
|
|
1082
1133
|
|
|
1083
1134
|
```
|
|
1084
|
-
GET /
|
|
1135
|
+
GET /MyTable/123.propertyName
|
|
1085
1136
|
```
|
|
1086
1137
|
|
|
1087
1138
|
#### Notes
|
|
1088
1139
|
|
|
1089
|
-
-
|
|
1090
|
-
-
|
|
1091
|
-
- When selecting a related attribute without filtering on it, the
|
|
1092
|
-
- The
|
|
1093
|
-
- Square brackets
|
|
1094
|
-
- `directURLMapping: true` can be set on a resource to change URL path handling semantics; see your schema configuration for details.
|
|
1140
|
+
- Only indexed attributes can be used as the primary filter attribute; when combining multiple attributes, only one needs to be indexed.
|
|
1141
|
+
- Relationship attributes are excluded from responses by default. Always use `select(` to include them.
|
|
1142
|
+
- When selecting a related attribute without filtering on it, the behavior is a LEFT JOIN — the property is omitted if the foreign key is null or references a non-existent record.
|
|
1143
|
+
- The suffixes `.json`, `.cbor`, `.msgpack`, and `.csv` in URL paths are reserved as content-type selectors and take precedence over a property of the same name.
|
|
1144
|
+
- Square brackets are preferred over parentheses when building grouped queries programmatically, because `[` and `]` are safely URL-encoded by standard encoding functions while `(` is not.
|
|
1095
1145
|
|
|
1096
1146
|
### 2.3 Real-Time Apps with WebSockets and Pub/Sub
|
|
1097
1147
|
|
|
@@ -1187,138 +1237,59 @@ export class Example extends Resource {
|
|
|
1187
1237
|
|
|
1188
1238
|
### 2.4 Checking Authentication
|
|
1189
1239
|
|
|
1190
|
-
Instructions for the agent to
|
|
1240
|
+
Instructions for the agent to handle user authentication, sessions, and JWT token issuance in Harper Resources.
|
|
1191
1241
|
|
|
1192
1242
|
#### When to Use
|
|
1193
1243
|
|
|
1194
|
-
Apply this rule when implementing
|
|
1244
|
+
Apply this rule when implementing login/logout flows, reading the current authenticated user, issuing or refreshing JWT tokens, or minting scoped tokens from a custom Resource. Use it whenever a Resource must gate behavior on identity or credentials. See [custom-resources.md](custom-resources.md) for the broader Resource authoring model.
|
|
1195
1245
|
|
|
1196
1246
|
#### How It Works
|
|
1197
1247
|
|
|
1198
|
-
1.
|
|
1199
|
-
|
|
1200
|
-
```javascript
|
|
1201
|
-
async get(target) {
|
|
1202
|
-
const user = this.getCurrentUser();
|
|
1203
|
-
if (!user) return new Response(null, { status: 401 });
|
|
1204
|
-
return { username: user.username, role: user.role };
|
|
1205
|
-
}
|
|
1206
|
-
```
|
|
1207
|
-
|
|
1208
|
-
The returned object exposes `username`, `role`, and `role.permission` flags.
|
|
1209
|
-
|
|
1210
|
-
2. **Enable sessions** before using session-based login. Set `authentication.enableSessions: true` in `harperdb-config.yaml`:
|
|
1211
|
-
|
|
1212
|
-
```yaml
|
|
1213
|
-
authentication:
|
|
1214
|
-
enableSessions: true
|
|
1215
|
-
```
|
|
1248
|
+
##### 1. Reading the Current Authenticated User
|
|
1216
1249
|
|
|
1217
|
-
|
|
1218
|
-
- Call `context.login(username, password)` to verify credentials and establish a session cookie on success.
|
|
1219
|
-
- To end a session, delete it via `context.session.delete(context.session.id)`.
|
|
1250
|
+
**In an instance method**, call `getCurrentUser()` to get the user associated with the current request, or `undefined` if unauthenticated. The returned object exposes `username`, `role`, and `role.permission` flags.
|
|
1220
1251
|
|
|
1221
|
-
|
|
1252
|
+
**In a static verb**, read the user from the `context` argument instead — `context.user`:
|
|
1222
1253
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
return new Response('Invalid credentials', { status: 403 });
|
|
1231
|
-
}
|
|
1232
|
-
return new Response('Logged in', { status: 200 });
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
export class SignOut extends Resource {
|
|
1237
|
-
async post() {
|
|
1238
|
-
const context = this.getContext();
|
|
1239
|
-
if (!context.session) return new Response(null, { status: 401 });
|
|
1240
|
-
await context.session.delete(context.session.id);
|
|
1241
|
-
return new Response('Logged out', { status: 200 });
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
```
|
|
1245
|
-
|
|
1246
|
-
5. **Issue JWTs for non-browser clients** (CLI tools, mobile apps, service-to-service). Cookie-based sessions are intended for browser clients. For other clients, mint tokens programmatically using `server.operation()`:
|
|
1247
|
-
|
|
1248
|
-
```javascript
|
|
1249
|
-
import { Resource, server } from 'harper';
|
|
1250
|
-
|
|
1251
|
-
export class IssueTokens extends Resource {
|
|
1252
|
-
static async get(_target, context) {
|
|
1253
|
-
const { operation_token, refresh_token } = await server.operation(
|
|
1254
|
-
{ operation: 'create_authentication_tokens' },
|
|
1255
|
-
context,
|
|
1256
|
-
true,
|
|
1257
|
-
);
|
|
1258
|
-
return { operation_token, refresh_token };
|
|
1259
|
-
}
|
|
1254
|
+
```javascript
|
|
1255
|
+
static async get(_target, context) {
|
|
1256
|
+
const user = context?.user;
|
|
1257
|
+
if (!user) return new Response(null, { status: 401 });
|
|
1258
|
+
return { username: user.username, role: user.role };
|
|
1259
|
+
}
|
|
1260
|
+
```
|
|
1260
1261
|
|
|
1261
|
-
|
|
1262
|
-
const { username, password } = await data;
|
|
1263
|
-
if (!username || !password) {
|
|
1264
|
-
return new Response('username and password required', { status: 400 });
|
|
1265
|
-
}
|
|
1266
|
-
const { operation_token, refresh_token } = await server.operation({
|
|
1267
|
-
operation: 'create_authentication_tokens',
|
|
1268
|
-
username,
|
|
1269
|
-
password,
|
|
1270
|
-
});
|
|
1271
|
-
return { operation_token, refresh_token };
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1262
|
+
##### 2. Reading the Full Context
|
|
1274
1263
|
|
|
1275
|
-
|
|
1276
|
-
static async post(_target, data) {
|
|
1277
|
-
const { refresh_token } = await data;
|
|
1278
|
-
if (!refresh_token) {
|
|
1279
|
-
return new Response('refresh_token required', { status: 400 });
|
|
1280
|
-
}
|
|
1281
|
-
const { operation_token } = await server.operation({
|
|
1282
|
-
operation: 'refresh_operation_token',
|
|
1283
|
-
refresh_token,
|
|
1284
|
-
});
|
|
1285
|
-
return { operation_token };
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
```
|
|
1264
|
+
**In an instance method**, call `getContext()` to retrieve the current context, which includes:
|
|
1289
1265
|
|
|
1290
|
-
|
|
1266
|
+
- `user` — User object with username, role, and authorization information
|
|
1267
|
+
- `transaction` — The current transaction
|
|
1291
1268
|
|
|
1292
|
-
|
|
1269
|
+
**In a static verb**, the context is the trailing argument:
|
|
1293
1270
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
operationTokenTimeout: 1d
|
|
1297
|
-
refreshTokenTimeout: 30d
|
|
1298
|
-
```
|
|
1271
|
+
- `(target, context)` for `get`/`delete`
|
|
1272
|
+
- `(target, data, context)` for `put`/`patch`/`post`
|
|
1299
1273
|
|
|
1300
|
-
|
|
1274
|
+
When triggered by HTTP, the context also exposes `url`, `method`, `headers`, `responseHeaders`, `pathname`, `host`, `ip`, `body`, `data`, `lastModified`, and `requestContext`.
|
|
1301
1275
|
|
|
1302
|
-
|
|
1276
|
+
##### 3. Handling Sessions and Login
|
|
1303
1277
|
|
|
1304
|
-
|
|
1278
|
+
Enable sessions in `harper-config.yaml`:
|
|
1305
1279
|
|
|
1306
|
-
```
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (!user) return new Response(null, { status: 401 });
|
|
1310
|
-
return { username: user.username, role: user.role };
|
|
1311
|
-
}
|
|
1280
|
+
```yaml
|
|
1281
|
+
authentication:
|
|
1282
|
+
enableSessions: true
|
|
1312
1283
|
```
|
|
1313
1284
|
|
|
1314
|
-
|
|
1285
|
+
Use `context.login` in a static `post` verb to verify credentials and establish a session cookie:
|
|
1315
1286
|
|
|
1316
1287
|
```javascript
|
|
1317
1288
|
export class SignIn extends Resource {
|
|
1318
|
-
async post(_target, data) {
|
|
1319
|
-
const
|
|
1289
|
+
static async post(_target, data, context) {
|
|
1290
|
+
const { username, password } = (await data) ?? {};
|
|
1320
1291
|
try {
|
|
1321
|
-
await context.login(
|
|
1292
|
+
await context.login(username, password);
|
|
1322
1293
|
} catch {
|
|
1323
1294
|
return new Response('Invalid credentials', { status: 403 });
|
|
1324
1295
|
}
|
|
@@ -1327,18 +1298,161 @@ export class SignIn extends Resource {
|
|
|
1327
1298
|
}
|
|
1328
1299
|
|
|
1329
1300
|
export class SignOut extends Resource {
|
|
1330
|
-
async post() {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
await context.session.delete(context.session.id);
|
|
1301
|
+
static async post(_target, _data, context) {
|
|
1302
|
+
if (!context?.session?.user) return new Response(null, { status: 401 });
|
|
1303
|
+
await context.session.update({ user: null });
|
|
1334
1304
|
return new Response('Logged out', { status: 200 });
|
|
1335
1305
|
}
|
|
1336
1306
|
}
|
|
1337
1307
|
```
|
|
1338
1308
|
|
|
1339
|
-
|
|
1309
|
+
- `context.login(username, password)` verifies credentials and sets the session cookie on success.
|
|
1310
|
+
- To end a session, call `context.session.update({ user: null })` — `update` is the session's only mutator.
|
|
1311
|
+
- `context.session` is an empty object (not `undefined`) when sessions are enabled and no session cookie is present; test `context?.session?.user` to detect an established session.
|
|
1312
|
+
- Cookie-based sessions are for browser clients. For non-browser clients, use JWT issuance.
|
|
1313
|
+
|
|
1314
|
+
##### 4. Creating Authentication Tokens
|
|
1315
|
+
|
|
1316
|
+
Call `create_authentication_tokens` with credentials in the body — no `Authorization` header is required when `username` and `password` are supplied:
|
|
1317
|
+
|
|
1318
|
+
```json
|
|
1319
|
+
{
|
|
1320
|
+
"operation": "create_authentication_tokens",
|
|
1321
|
+
"username": "username",
|
|
1322
|
+
"password": "password"
|
|
1323
|
+
}
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
Response:
|
|
1327
|
+
|
|
1328
|
+
```json
|
|
1329
|
+
{
|
|
1330
|
+
"operation_token": "<jwt-operation-token>",
|
|
1331
|
+
"refresh_token": "<jwt-refresh-token>"
|
|
1332
|
+
}
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
Pass the `operation_token` as a `Bearer` token on subsequent requests:
|
|
1336
|
+
|
|
1337
|
+
```bash
|
|
1338
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
1339
|
+
--header 'Content-Type: application/json' \
|
|
1340
|
+
--header 'Authorization: Bearer <operation_token>' \
|
|
1341
|
+
--data-raw '{
|
|
1342
|
+
"operation": "search_by_hash",
|
|
1343
|
+
"schema": "dev",
|
|
1344
|
+
"table": "dog",
|
|
1345
|
+
"hash_values": [1],
|
|
1346
|
+
"get_attributes": ["*"]
|
|
1347
|
+
}'
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
##### 5. Refreshing the Operation Token
|
|
1351
|
+
|
|
1352
|
+
When the `operation_token` expires, pass the `refresh_token` as `Bearer <refresh_token>` and call `refresh_operation_token`:
|
|
1353
|
+
|
|
1354
|
+
```bash
|
|
1355
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
1356
|
+
--header 'Content-Type: application/json' \
|
|
1357
|
+
--header 'Authorization: Bearer <refresh_token>' \
|
|
1358
|
+
--data-raw '{
|
|
1359
|
+
"operation": "refresh_operation_token"
|
|
1360
|
+
}'
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
Response:
|
|
1364
|
+
|
|
1365
|
+
```json
|
|
1366
|
+
{
|
|
1367
|
+
"operation_token": "<new-jwt-operation-token>"
|
|
1368
|
+
}
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
When both tokens have expired, call `create_authentication_tokens` again with username and password.
|
|
1372
|
+
|
|
1373
|
+
##### 6. Minting Scoped Tokens
|
|
1374
|
+
|
|
1375
|
+
A `super_user` can mint a scoped token with embedded permissions using `create_authentication_tokens` with an inline `role`. The minter must be authenticated; no `password` may be included in the body. Use `add_role`-style `permission` structure:
|
|
1376
|
+
|
|
1377
|
+
```json
|
|
1378
|
+
{
|
|
1379
|
+
"operation": "create_authentication_tokens",
|
|
1380
|
+
"username": "reporting-service",
|
|
1381
|
+
"role": {
|
|
1382
|
+
"permission": {
|
|
1383
|
+
"operations": ["read_only"],
|
|
1384
|
+
"dev": {
|
|
1385
|
+
"tables": {
|
|
1386
|
+
"dog": {
|
|
1387
|
+
"read": true,
|
|
1388
|
+
"insert": false,
|
|
1389
|
+
"update": false,
|
|
1390
|
+
"delete": false,
|
|
1391
|
+
"attribute_permissions": []
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
"expires_in": "7d"
|
|
1398
|
+
}
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
Authenticate the mint request with Basic Authentication or an existing `super_user` `operation_token`:
|
|
1402
|
+
|
|
1403
|
+
```bash
|
|
1404
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
1405
|
+
--header 'Content-Type: application/json' \
|
|
1406
|
+
--header 'Authorization: Basic <base64 of super_user:password>' \
|
|
1407
|
+
--data-raw '{
|
|
1408
|
+
"operation": "create_authentication_tokens",
|
|
1409
|
+
"username": "reporting-service",
|
|
1410
|
+
"role": { "permission": { "operations": ["read_only"] } },
|
|
1411
|
+
"expires_in": "7d"
|
|
1412
|
+
}'
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
Key constraints for scoped tokens:
|
|
1416
|
+
|
|
1417
|
+
- `username` is attribution only and must not name an existing user.
|
|
1418
|
+
- No refresh token is issued; no user record is created.
|
|
1419
|
+
- Scoped tokens cannot be revoked before expiry — choose `expires_in` carefully and prefer short lifetimes.
|
|
1420
|
+
- `super_user` and `cluster_user` are always forced to `false` in the embedded role.
|
|
1421
|
+
- In **mixed-version** clusters, only nodes with scoped-token support accept these tokens; older nodes reject them with 401.
|
|
1422
|
+
|
|
1423
|
+
##### 7. Issuing Tokens from a Custom Resource
|
|
1424
|
+
|
|
1425
|
+
Use `server.operation()` to mint tokens programmatically inside a Resource. Pass `authorize: true` as the **third argument** when the operation should run as the current authenticated user:
|
|
1340
1426
|
|
|
1341
1427
|
```javascript
|
|
1428
|
+
import { Resource, server } from 'harper';
|
|
1429
|
+
|
|
1430
|
+
export class IssueTokens extends Resource {
|
|
1431
|
+
static async get(_target, context) {
|
|
1432
|
+
// Issue tokens for the current authenticated user
|
|
1433
|
+
const { operation_token, refresh_token } = await server.operation(
|
|
1434
|
+
{ operation: 'create_authentication_tokens' },
|
|
1435
|
+
context,
|
|
1436
|
+
true, // third argument: authorize as current user
|
|
1437
|
+
);
|
|
1438
|
+
return { operation_token, refresh_token };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
static async post(_target, data) {
|
|
1442
|
+
// Issue tokens from credentials supplied in the body
|
|
1443
|
+
const { username, password } = await data;
|
|
1444
|
+
if (!username || !password) {
|
|
1445
|
+
return new Response('username and password required', { status: 400 });
|
|
1446
|
+
}
|
|
1447
|
+
const { operation_token, refresh_token } = await server.operation({
|
|
1448
|
+
operation: 'create_authentication_tokens',
|
|
1449
|
+
username,
|
|
1450
|
+
password,
|
|
1451
|
+
});
|
|
1452
|
+
return { operation_token, refresh_token };
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1342
1456
|
export class RefreshJWT extends Resource {
|
|
1343
1457
|
static async post(_target, data) {
|
|
1344
1458
|
const { refresh_token } = await data;
|
|
@@ -1354,12 +1468,76 @@ export class RefreshJWT extends Resource {
|
|
|
1354
1468
|
}
|
|
1355
1469
|
```
|
|
1356
1470
|
|
|
1471
|
+
Omit the third argument (or pass `false`) when the operation supplies its own credentials.
|
|
1472
|
+
|
|
1473
|
+
##### 8. Configuring Token Expiry
|
|
1474
|
+
|
|
1475
|
+
Set timeouts in `harper-config.yaml` under the `authentication` section:
|
|
1476
|
+
|
|
1477
|
+
```yaml
|
|
1478
|
+
authentication:
|
|
1479
|
+
operationTokenTimeout: 1d # Default: 1 day
|
|
1480
|
+
refreshTokenTimeout: 30d # Default: 30 days
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
Valid duration strings follow the `jsonwebtoken` package format (e.g., `1d`, `12h`, `60m`). The `expires_in` field on scoped token minting accepts the same format.
|
|
1484
|
+
|
|
1485
|
+
#### Examples
|
|
1486
|
+
|
|
1487
|
+
##### Full Sign-In / Sign-Out Resource
|
|
1488
|
+
|
|
1489
|
+
```javascript
|
|
1490
|
+
export class SignIn extends Resource {
|
|
1491
|
+
static async post(_target, data, context) {
|
|
1492
|
+
const { username, password } = (await data) ?? {};
|
|
1493
|
+
try {
|
|
1494
|
+
await context.login(username, password);
|
|
1495
|
+
} catch {
|
|
1496
|
+
return new Response('Invalid credentials', { status: 403 });
|
|
1497
|
+
}
|
|
1498
|
+
return new Response('Logged in', { status: 200 });
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
export class SignOut extends Resource {
|
|
1503
|
+
static async post(_target, _data, context) {
|
|
1504
|
+
if (!context?.session?.user) return new Response(null, { status: 401 });
|
|
1505
|
+
await context.session.update({ user: null });
|
|
1506
|
+
return new Response('Logged out', { status: 200 });
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
##### Reading the Current Authenticated User in a Static Verb
|
|
1512
|
+
|
|
1513
|
+
```javascript
|
|
1514
|
+
static async get(_target, context) {
|
|
1515
|
+
const user = context?.user;
|
|
1516
|
+
if (!user) return new Response(null, { status: 401 });
|
|
1517
|
+
return { username: user.username, role: user.role };
|
|
1518
|
+
}
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
##### Minting Tokens via cURL
|
|
1522
|
+
|
|
1523
|
+
```bash
|
|
1524
|
+
curl --location --request POST 'http://localhost:9925' \
|
|
1525
|
+
--header 'Content-Type: application/json' \
|
|
1526
|
+
--data-raw '{
|
|
1527
|
+
"operation": "create_authentication_tokens",
|
|
1528
|
+
"username": "username",
|
|
1529
|
+
"password": "password"
|
|
1530
|
+
}'
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1357
1533
|
#### Notes
|
|
1358
1534
|
|
|
1359
|
-
-
|
|
1360
|
-
-
|
|
1361
|
-
-
|
|
1362
|
-
-
|
|
1535
|
+
- JWT authentication is **preferred over Basic Auth** when you want to avoid sending credentials on every request, your client can store tokens, or you have multiple sequential requests. For simple or **server-to-server** scenarios, use **Basic Authentication**.
|
|
1536
|
+
- Always use **HTTPS** in production to protect tokens in transit.
|
|
1537
|
+
- Treat tokens like passwords. If a token is compromised, it remains valid until expiry — use shorter `operationTokenTimeout` values in high-security environments.
|
|
1538
|
+
- `context.login` and `context.session` require `enableSessions: true` in `harperdb-config.yaml`; they are for browser clients only.
|
|
1539
|
+
- The `server.operation()` third argument (`authorize: true`) attributes the operation to — and permission-checks against — the current authenticated user. Omit it when supplying credentials directly in the operation body.
|
|
1540
|
+
- In **mixed-version** clusters, scoped tokens are only accepted by nodes that support them; older nodes return 401.
|
|
1363
1541
|
|
|
1364
1542
|
## 3. Logic & Extension
|
|
1365
1543
|
|
|
@@ -1369,11 +1547,11 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
|
|
|
1369
1547
|
|
|
1370
1548
|
#### When to Use
|
|
1371
1549
|
|
|
1372
|
-
Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed
|
|
1550
|
+
Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed resource or when a specific URL shape is required.
|
|
1373
1551
|
|
|
1374
1552
|
#### How It Works
|
|
1375
1553
|
|
|
1376
|
-
1. **Import `Resource` from `harper
|
|
1554
|
+
1. **Import `Resource` from the `harper` package**: Always import explicitly rather than relying on globals.
|
|
1377
1555
|
|
|
1378
1556
|
```javascript
|
|
1379
1557
|
import { tables, Resource } from 'harper';
|
|
@@ -1391,7 +1569,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1391
1569
|
}
|
|
1392
1570
|
```
|
|
1393
1571
|
|
|
1394
|
-
3. **Use `async` static methods
|
|
1572
|
+
3. **Use `async` static methods to call external services**: Return or forward the response directly.
|
|
1395
1573
|
|
|
1396
1574
|
```javascript
|
|
1397
1575
|
export class MyExternalData extends Resource {
|
|
@@ -1409,7 +1587,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1409
1587
|
}
|
|
1410
1588
|
```
|
|
1411
1589
|
|
|
1412
|
-
4. **Export the class to
|
|
1590
|
+
4. **Export the class to expose it as an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
|
|
1413
1591
|
|
|
1414
1592
|
| Export form | URL | Notes |
|
|
1415
1593
|
| ------------------------------------------- | --------------- | --------------------------------------------------------------- |
|
|
@@ -1420,12 +1598,17 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1420
1598
|
| `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
|
|
1421
1599
|
| `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
|
|
1422
1600
|
|
|
1423
|
-
|
|
1601
|
+
5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
|
|
1424
1602
|
|
|
1425
|
-
|
|
1603
|
+
```javascript
|
|
1604
|
+
server.resources.set('my-path', Foo);
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
6. **Declare dynamic path segments with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
|
|
1426
1608
|
|
|
1427
1609
|
```javascript
|
|
1428
1610
|
export class Widget extends Resource {
|
|
1611
|
+
// GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
|
|
1429
1612
|
static path = '/widget/:id/action/:action';
|
|
1430
1613
|
static get(target) {
|
|
1431
1614
|
return { id: target.id, action: target.action };
|
|
@@ -1433,22 +1616,11 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1433
1616
|
}
|
|
1434
1617
|
```
|
|
1435
1618
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` when the path cannot be known at export time.
|
|
1439
|
-
|
|
1440
|
-
```javascript
|
|
1441
|
-
server.resources.set('my-path', Foo);
|
|
1442
|
-
```
|
|
1443
|
-
|
|
1444
|
-
7. **Optionally source a table from a custom resource**: Use the resource as a caching layer for a local table.
|
|
1445
|
-
```javascript
|
|
1446
|
-
tables.MyCache.sourcedFrom(MyExternalData);
|
|
1447
|
-
```
|
|
1619
|
+
7. **Resolve path precedence correctly**: Exact and static paths always win over parameterized ones. Among parameterized routes, more specific paths win: a literal segment beats a `:param`, which beats a `*` wildcard, compared left to right.
|
|
1448
1620
|
|
|
1449
1621
|
#### Examples
|
|
1450
1622
|
|
|
1451
|
-
|
|
1623
|
+
**Wrapping an external API and using it as a cache source:**
|
|
1452
1624
|
|
|
1453
1625
|
```javascript
|
|
1454
1626
|
import { tables, Resource } from 'harper';
|
|
@@ -1471,19 +1643,9 @@ export class MyExternalData extends Resource {
|
|
|
1471
1643
|
tables.MyCache.sourcedFrom(MyExternalData);
|
|
1472
1644
|
```
|
|
1473
1645
|
|
|
1474
|
-
|
|
1646
|
+
**Catch-all wildcard path:**
|
|
1475
1647
|
|
|
1476
1648
|
```javascript
|
|
1477
|
-
import { Resource } from 'harper';
|
|
1478
|
-
|
|
1479
|
-
export class Widget extends Resource {
|
|
1480
|
-
// GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
|
|
1481
|
-
static path = '/widget/:id/action/:action';
|
|
1482
|
-
static get(target) {
|
|
1483
|
-
return { id: target.id, action: target.action };
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
1649
|
export class Files extends Resource {
|
|
1488
1650
|
// GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
|
|
1489
1651
|
static path = '/files/*rest';
|
|
@@ -1493,28 +1655,27 @@ export class Files extends Resource {
|
|
|
1493
1655
|
}
|
|
1494
1656
|
```
|
|
1495
1657
|
|
|
1496
|
-
|
|
1658
|
+
**Root-relative fixed route:**
|
|
1497
1659
|
|
|
1498
1660
|
```javascript
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
export class Foo extends Resource {
|
|
1661
|
+
export class AcmeChallenge extends Resource {
|
|
1662
|
+
static path = '/.well-known/acme-challenge/:token';
|
|
1502
1663
|
static get(target) {
|
|
1503
|
-
return {
|
|
1664
|
+
return { token: target.token };
|
|
1504
1665
|
}
|
|
1505
1666
|
}
|
|
1506
|
-
|
|
1507
|
-
server.resources.set('my-path', Foo);
|
|
1508
1667
|
```
|
|
1509
1668
|
|
|
1510
1669
|
#### Notes
|
|
1511
1670
|
|
|
1512
|
-
-
|
|
1513
|
-
-
|
|
1514
|
-
-
|
|
1515
|
-
-
|
|
1671
|
+
- URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
|
|
1672
|
+
- A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
|
|
1673
|
+
- A leading `./` or a bare name in `static path` resolves relative to the component directory.
|
|
1674
|
+
- A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
|
|
1675
|
+
- `static path` takes precedence over the export name when both are present.
|
|
1676
|
+
- Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as URI templates.
|
|
1677
|
+
- When a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
|
|
1516
1678
|
- Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
|
|
1517
|
-
- Harper runs as a single process — `tables`, `databases`, and other APIs are the same live, process-wide objects regardless of which component accesses them.
|
|
1518
1679
|
|
|
1519
1680
|
### 3.2 Extending Tables
|
|
1520
1681
|
|
|
@@ -1622,23 +1783,25 @@ if (!authorized) {
|
|
|
1622
1783
|
|
|
1623
1784
|
### 3.3 Programmatic Table Requests
|
|
1624
1785
|
|
|
1625
|
-
Instructions for the agent to interact with Harper tables programmatically using the `tables` object
|
|
1786
|
+
Instructions for the agent to interact with Harper tables programmatically using the `tables` object, the Query API, and transactions.
|
|
1626
1787
|
|
|
1627
1788
|
#### When to Use
|
|
1628
1789
|
|
|
1629
|
-
Apply this rule when writing server-side code that reads from or writes to
|
|
1790
|
+
Apply this rule when writing server-side Harper component code that reads from or writes to tables directly — for example, in HTTP handlers, background jobs, timers, or SSR render functions. Use it whenever you need to construct queries with `conditions`, `sort`, `select`, `limit`, or `offset`, or when you need explicit transaction control via `transaction()`.
|
|
1630
1791
|
|
|
1631
1792
|
#### How It Works
|
|
1632
1793
|
|
|
1633
|
-
1. **Import `tables
|
|
1794
|
+
1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a named property of `tables`. Each property is the table class implementing the Resource API.
|
|
1634
1795
|
|
|
1635
1796
|
```javascript
|
|
1636
|
-
import { tables } from 'harper';
|
|
1797
|
+
import { tables, transaction } from 'harper';
|
|
1637
1798
|
const { Product } = tables;
|
|
1638
|
-
//
|
|
1799
|
+
// equivalent to: databases.data.Product
|
|
1639
1800
|
```
|
|
1640
1801
|
|
|
1641
|
-
|
|
1802
|
+
For standalone components, run `npm link harper` so imports resolve to the live runtime.
|
|
1803
|
+
|
|
1804
|
+
2. **Define your schema with `@table`**: Tables must be declared in `schema.graphql`. Mark attributes you intend to sort or filter on with `@indexed`.
|
|
1642
1805
|
|
|
1643
1806
|
```graphql
|
|
1644
1807
|
type Product @table {
|
|
@@ -1648,15 +1811,7 @@ Apply this rule when writing server-side code that reads from or writes to Harpe
|
|
|
1648
1811
|
}
|
|
1649
1812
|
```
|
|
1650
1813
|
|
|
1651
|
-
3. **
|
|
1652
|
-
|
|
1653
|
-
```javascript
|
|
1654
|
-
const created = await Product.create({ name: 'Shirt', price: 9.5 });
|
|
1655
|
-
await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
|
|
1656
|
-
const record = await Product.get(created.id);
|
|
1657
|
-
```
|
|
1658
|
-
|
|
1659
|
-
4. **Query with `search(`**: Pass a query object to `Product.search(query)`. It returns an async iterable.
|
|
1814
|
+
3. **Query records with `search(`**: Pass a Query object to `search(`. Iterate results with `for await`.
|
|
1660
1815
|
|
|
1661
1816
|
```javascript
|
|
1662
1817
|
const query = {
|
|
@@ -1667,7 +1822,7 @@ Apply this rule when writing server-side code that reads from or writes to Harpe
|
|
|
1667
1822
|
}
|
|
1668
1823
|
```
|
|
1669
1824
|
|
|
1670
|
-
|
|
1825
|
+
4. **Build `conditions`**: Each condition object supports these properties:
|
|
1671
1826
|
|
|
1672
1827
|
| Property | Description |
|
|
1673
1828
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -1677,29 +1832,31 @@ Apply this rule when writing server-side code that reads from or writes to Harpe
|
|
|
1677
1832
|
| `conditions` | Nested conditions array |
|
|
1678
1833
|
| `operator` | `and` (default) or `or` for the nested `conditions` |
|
|
1679
1834
|
|
|
1680
|
-
|
|
1835
|
+
5. **Control result shape with `select`**: Pass an array of property names, a single string, or nested objects for relationships.
|
|
1681
1836
|
|
|
1682
1837
|
```javascript
|
|
1683
|
-
//
|
|
1838
|
+
// Scalar fields only
|
|
1684
1839
|
Product.search({ select: ['name', 'price'] });
|
|
1685
1840
|
|
|
1686
|
-
//
|
|
1841
|
+
// Partial related record
|
|
1687
1842
|
Book.get({ id: 42, select: ['id', 'title', { name: 'author', select: ['name'] }] });
|
|
1688
1843
|
```
|
|
1689
1844
|
|
|
1690
|
-
Special `select` values:
|
|
1691
|
-
- `$id` — returns the primary key regardless of its name
|
|
1692
|
-
- `$updatedtime` — returns the last-updated timestamp
|
|
1693
|
-
- `$distance` — returns the computed distance when querying a vector index
|
|
1845
|
+
Special `select` values: `$id`, `$updatedtime`, `$distance`.
|
|
1694
1846
|
|
|
1695
|
-
|
|
1847
|
+
6. **Paginate with `limit` and `offset`**:
|
|
1696
1848
|
|
|
1697
1849
|
```javascript
|
|
1698
|
-
|
|
1699
|
-
record.addTo('quantity', -1);
|
|
1850
|
+
Product.search({ conditions: [...], limit: 20, offset: 40 });
|
|
1700
1851
|
```
|
|
1701
1852
|
|
|
1702
|
-
|
|
1853
|
+
7. **Sort with `sort`**: The `sort` object accepts `attribute`, `descending` (default `false`), and `next` for tie-breaking. Harper uses an index for sort order — the sort `attribute` must be `@indexed`, **or** at least one entry in `conditions` must be present.
|
|
1854
|
+
|
|
1855
|
+
Sorting by a non-indexed attribute with zero conditions raises:
|
|
1856
|
+
|
|
1857
|
+
> `HdbError: <attribute> is not indexed and not combined with any other conditions`
|
|
1858
|
+
|
|
1859
|
+
Note: `@primaryKey` alone is treated as not indexed for sort purposes. To scan the whole table in primary-key order, add an open-ended condition:
|
|
1703
1860
|
|
|
1704
1861
|
```javascript
|
|
1705
1862
|
Product.search({
|
|
@@ -1708,15 +1865,78 @@ Apply this rule when writing server-side code that reads from or writes to Harpe
|
|
|
1708
1865
|
});
|
|
1709
1866
|
```
|
|
1710
1867
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1868
|
+
Alternatively, pass `allowFullScan: true` to permit an unconditional ordered scan, or omit `sort` entirely to iterate without an index requirement.
|
|
1869
|
+
|
|
1870
|
+
8. **Debug query planning with `explain` and `enforceExecutionOrder`**:
|
|
1871
|
+
- `explain: true` — returns conditions reordered as Harper will execute them.
|
|
1872
|
+
- `enforceExecutionOrder: true` — forces conditions to execute in the order supplied, disabling automatic re-ordering.
|
|
1873
|
+
|
|
1874
|
+
9. **Use `addTo` for concurrent-safe numeric updates**: `addTo` uses CRDT incrementation, safe across threads and nodes.
|
|
1875
|
+
|
|
1876
|
+
```javascript
|
|
1877
|
+
const record = await Product.update(32);
|
|
1878
|
+
record.addTo('quantity', -1);
|
|
1879
|
+
```
|
|
1880
|
+
|
|
1881
|
+
10. **Wrap background work in `transaction()`**: HTTP handlers get a transaction automatically. Use `transaction()` explicitly for timers, background jobs, or any code outside a request context. Always `await` the call and `catch` errors.
|
|
1882
|
+
|
|
1883
|
+
```javascript
|
|
1884
|
+
await transaction(async (txn) => {
|
|
1885
|
+
for (let item of data) {
|
|
1886
|
+
await MyTable.put(item, txn);
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
```
|
|
1890
|
+
|
|
1891
|
+
The `txn` object exposes:
|
|
1892
|
+
|
|
1893
|
+
| Member | Description |
|
|
1894
|
+
| --------------------- | ------------------------------------------------------ |
|
|
1895
|
+
| `commit()` | Commits the current transaction |
|
|
1896
|
+
| `abort()` | Aborts and resets the transaction |
|
|
1897
|
+
| `resetReadSnapshot()` | Resets the read snapshot to the latest committed state |
|
|
1898
|
+
| `timestamp` | Timestamp associated with the current transaction |
|
|
1899
|
+
|
|
1900
|
+
**Atomicity**: all tables in the same database share one transactional context — writes across multiple tables commit atomically. Tables in different databases each get their own transaction with no cross-database atomicity guarantee.
|
|
1901
|
+
|
|
1902
|
+
If `transaction()` is called with a context that already has an active transaction, it reuses that transaction — safe to call defensively.
|
|
1903
|
+
|
|
1904
|
+
11. **Keep `harper` external when bundling for SSR**: In `vite.config`, mark `harper` as external so it resolves to the runtime rather than being bundled.
|
|
1905
|
+
|
|
1906
|
+
```javascript
|
|
1907
|
+
// vite.config
|
|
1908
|
+
ssr: {
|
|
1909
|
+
external: ['harper'];
|
|
1910
|
+
}
|
|
1911
|
+
```
|
|
1716
1912
|
|
|
1717
1913
|
#### Examples
|
|
1718
1914
|
|
|
1719
|
-
|
|
1915
|
+
##### Full CRUD sequence
|
|
1916
|
+
|
|
1917
|
+
```javascript
|
|
1918
|
+
import { tables } from 'harper';
|
|
1919
|
+
const { Product } = tables;
|
|
1920
|
+
|
|
1921
|
+
// Create
|
|
1922
|
+
const created = await Product.create({ name: 'Shirt', price: 9.5 });
|
|
1923
|
+
|
|
1924
|
+
// Patch
|
|
1925
|
+
await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
|
|
1926
|
+
|
|
1927
|
+
// Retrieve by primary key
|
|
1928
|
+
const record = await Product.get(created.id);
|
|
1929
|
+
|
|
1930
|
+
// Query with conditions
|
|
1931
|
+
const query = {
|
|
1932
|
+
conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
|
|
1933
|
+
};
|
|
1934
|
+
for await (const record of Product.search(query)) {
|
|
1935
|
+
// process record
|
|
1936
|
+
}
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
##### Nested conditions with `or`
|
|
1720
1940
|
|
|
1721
1941
|
```javascript
|
|
1722
1942
|
Product.search({
|
|
@@ -1733,25 +1953,41 @@ Product.search({
|
|
|
1733
1953
|
});
|
|
1734
1954
|
```
|
|
1735
1955
|
|
|
1736
|
-
|
|
1956
|
+
##### Chained attribute reference (join/relationship)
|
|
1737
1957
|
|
|
1738
1958
|
```javascript
|
|
1739
1959
|
Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
|
|
1740
1960
|
```
|
|
1741
1961
|
|
|
1742
|
-
|
|
1962
|
+
##### Background job with `transaction()`
|
|
1743
1963
|
|
|
1744
1964
|
```javascript
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1965
|
+
import { isMainThread } from 'node:worker_threads';
|
|
1966
|
+
import { tables, transaction } from 'harper';
|
|
1967
|
+
const { MyTable } = tables;
|
|
1968
|
+
|
|
1969
|
+
if (isMainThread) {
|
|
1970
|
+
let running = false;
|
|
1971
|
+
setInterval(async () => {
|
|
1972
|
+
if (running) return;
|
|
1973
|
+
running = true;
|
|
1974
|
+
try {
|
|
1975
|
+
let data = await (await fetch('https://example.com/data')).json();
|
|
1976
|
+
await transaction(async (txn) => {
|
|
1977
|
+
for (let item of data) {
|
|
1978
|
+
await MyTable.put(item, txn);
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
logger.error('hourly import failed', error);
|
|
1983
|
+
} finally {
|
|
1984
|
+
running = false;
|
|
1985
|
+
}
|
|
1986
|
+
}, 3600000);
|
|
1987
|
+
}
|
|
1752
1988
|
```
|
|
1753
1989
|
|
|
1754
|
-
|
|
1990
|
+
##### SSR render with `tables`
|
|
1755
1991
|
|
|
1756
1992
|
```typescript
|
|
1757
1993
|
import { tables } from 'harper';
|
|
@@ -1764,12 +2000,10 @@ export async function render(url: string): Promise<string> {
|
|
|
1764
2000
|
|
|
1765
2001
|
#### Notes
|
|
1766
2002
|
|
|
1767
|
-
-
|
|
1768
|
-
-
|
|
1769
|
-
-
|
|
1770
|
-
-
|
|
1771
|
-
- `tables` and `databases.data` reference the same live objects — a record written through one component is immediately visible to all others.
|
|
1772
|
-
- Keep `harper` external when bundling for SSR (e.g. `ssr: { external: ['harper'] }` in `vite.config`) so it resolves to the runtime.
|
|
2003
|
+
- `tables` and `databases` do **not** automatically apply role permissions — calls run in a trusted server-side context. Apply your own authorization controls before exposing results.
|
|
2004
|
+
- Destructive operations (`update`, `patch`, `delete`) act on live data and are not easily reversible. Scope them with specific `conditions` and validate the affected set before writing.
|
|
2005
|
+
- `tables` is the same live, process-wide object whether accessed as a global or via `import { tables } from 'harper'`. A record written through one component is immediately visible to every other.
|
|
2006
|
+
- Run `npm link harper` for components in their own directory to ensure typings match the running installation.
|
|
1773
2007
|
|
|
1774
2008
|
### 3.4 TypeScript Type Stripping in Harper
|
|
1775
2009
|
|
|
@@ -1988,11 +2222,11 @@ await fetch('http://localhost:9926/JokeCache/1', {
|
|
|
1988
2222
|
|
|
1989
2223
|
### 4.1 Deploying to Harper Fabric
|
|
1990
2224
|
|
|
1991
|
-
Instructions for the agent to follow when deploying a Harper application to a remote Harper Fabric cluster.
|
|
2225
|
+
Instructions for the agent to follow when deploying a Harper application to a remote Harper Fabric cloud cluster.
|
|
1992
2226
|
|
|
1993
2227
|
#### When to Use
|
|
1994
2228
|
|
|
1995
|
-
Apply this rule when deploying a Harper application to a remote Harper Fabric cluster or any remote Harper instance. This includes first-time deploys, redeployments, rollbacks,
|
|
2229
|
+
Apply this rule when deploying a Harper application to a remote Harper Fabric cluster or any remote Harper instance. This includes first-time deploys, redeployments, rollbacks, CI/CD pipeline deploys, and provisioning credentials for private repositories. See [creating-a-fabric-account-and-cluster.md](creating-a-fabric-account-and-cluster.md) for setting up the cluster before deploying.
|
|
1996
2230
|
|
|
1997
2231
|
#### How It Works
|
|
1998
2232
|
|
|
@@ -2003,7 +2237,7 @@ Apply this rule when deploying a Harper application to a remote Harper Fabric cl
|
|
|
2003
2237
|
# Provide cluster username and password when prompted
|
|
2004
2238
|
```
|
|
2005
2239
|
|
|
2006
|
-
2. **Deploy the application**:
|
|
2240
|
+
2. **Deploy the application**: After login, run `harper deploy` without repeating credentials. Set `restart=true` and `replicated=true` for a production deploy.
|
|
2007
2241
|
|
|
2008
2242
|
```bash
|
|
2009
2243
|
harper deploy \
|
|
@@ -2014,30 +2248,42 @@ Apply this rule when deploying a Harper application to a remote Harper Fabric cl
|
|
|
2014
2248
|
replicated=true
|
|
2015
2249
|
```
|
|
2016
2250
|
|
|
2017
|
-
3. **
|
|
2251
|
+
3. **Use environment variables for CI/CD**: Instead of `harper login`, export credentials as environment variables before calling `harper deploy`.
|
|
2018
2252
|
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2253
|
+
```bash
|
|
2254
|
+
export HARPER_CLI_USERNAME=<username>
|
|
2255
|
+
export HARPER_CLI_PASSWORD=<password>
|
|
2256
|
+
harper deploy \
|
|
2257
|
+
project=<name> \
|
|
2258
|
+
package=<package> \
|
|
2259
|
+
target=<remote> \
|
|
2260
|
+
restart=true \
|
|
2261
|
+
replicated=true
|
|
2262
|
+
```
|
|
2263
|
+
|
|
2264
|
+
4. **Choose a package source**: The `package` field accepts any valid npm dependency value. Select the form that matches your source:
|
|
2265
|
+
|
|
2266
|
+
| Source | `package` value |
|
|
2267
|
+
| ----------------------- | ---------------------------------------------------- |
|
|
2268
|
+
| Current local directory | Omit `package` |
|
|
2269
|
+
| npm package | `"@harperdb/status-check"` |
|
|
2270
|
+
| GitHub (public) | `"HarperFast/status-check"` or full URL |
|
|
2271
|
+
| Private repo (SSH) | `"git+ssh://git@github.com:HarperDB/secret-app.git"` |
|
|
2272
|
+
| Tarball | `"https://example.com/application.tar.gz"` |
|
|
2027
2273
|
|
|
2028
2274
|
For git tags, use the `semver` directive:
|
|
2029
2275
|
|
|
2030
2276
|
```
|
|
2031
|
-
|
|
2277
|
+
HarperFast/application-template#semver:v1.0.0
|
|
2032
2278
|
```
|
|
2033
2279
|
|
|
2034
|
-
|
|
2280
|
+
5. **Deploy by reference for reproducible deploys**: Pass `by_ref=true` to send a pinned git SHA instead of uploading a snapshot. The cluster fetches and builds from that exact commit.
|
|
2035
2281
|
|
|
2036
2282
|
```bash
|
|
2037
2283
|
harper deploy by_ref=true restart=true replicated=true
|
|
2038
2284
|
```
|
|
2039
2285
|
|
|
2040
|
-
Use `ref` to
|
|
2286
|
+
Use `ref` to target a specific commit, tag, or branch (resolved to a full SHA before sending):
|
|
2041
2287
|
|
|
2042
2288
|
```bash
|
|
2043
2289
|
# Deploy a specific tag
|
|
@@ -2047,53 +2293,32 @@ Apply this rule when deploying a Harper application to a remote Harper Fabric cl
|
|
|
2047
2293
|
harper deploy ref=9f8c2a1 restart=true replicated=true
|
|
2048
2294
|
```
|
|
2049
2295
|
|
|
2050
|
-
**Key `
|
|
2296
|
+
**Key constraints for `ref` values:**
|
|
2297
|
+
- Must name something a clone can fetch: `refs/heads/*` and `refs/tags/*`, or a bare branch or tag name.
|
|
2298
|
+
- Anything else (e.g., `refs/pull/123/head`) is rejected up front.
|
|
2299
|
+
- If a ref can't be resolved, the deploy stops — run `git fetch` and retry, or pass a full commit SHA.
|
|
2300
|
+
- Commit and push before deploying: the cluster clones from the remote and only sees pushed commits.
|
|
2051
2301
|
|
|
2052
|
-
|
|
2053
|
-
| ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
2054
|
-
| `by_ref` | — | Build the package reference from the local repository |
|
|
2055
|
-
| `ref` | optional | Deploy a specific commit, tag, or branch instead of `HEAD`. Implies `by_ref`. |
|
|
2056
|
-
| `credential` | optional | Set to `true` to authenticate the clone with the stored credential for the repository's host. Omit for public repositories. |
|
|
2057
|
-
|
|
2058
|
-
> Commit and push before deploying by reference. The cluster clones from the remote and only sees pushed commits. Run `git fetch` if the unpushed-commit check fires for a commit you know you pushed.
|
|
2059
|
-
|
|
2060
|
-
5. **Authenticate for CI/CD**: Use environment variables instead of `harper login` for automated pipelines.
|
|
2302
|
+
6. **Deploy private repositories by reference**: Pass `credential=true` alongside `by_ref=true`. The CLI attaches a credentials reference; the cluster resolves the secret in memory at clone time — no token travels in the operation body or lands on disk.
|
|
2061
2303
|
|
|
2062
2304
|
```bash
|
|
2063
|
-
|
|
2064
|
-
export HARPER_CLI_PASSWORD=<password>
|
|
2065
|
-
harper deploy \
|
|
2066
|
-
project=<name> \
|
|
2067
|
-
package=<package> \
|
|
2068
|
-
target=<remote> \
|
|
2069
|
-
restart=true \
|
|
2070
|
-
replicated=true
|
|
2305
|
+
harper deploy by_ref=true credential=true restart=true replicated=true
|
|
2071
2306
|
```
|
|
2072
2307
|
|
|
2073
|
-
|
|
2308
|
+
7. **Provision a deploy credential for private sources**: Run `harper deploy setup=true` once per component and source. This is interactive and requires **super_user** — run it with an administrative credential, not the CI identity.
|
|
2074
2309
|
|
|
2075
2310
|
```bash
|
|
2076
2311
|
harper deploy setup=true
|
|
2077
2312
|
```
|
|
2078
2313
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2314
|
+
This command:
|
|
2315
|
+
1. Fetches the cluster's public key with `get_secrets_public_key`.
|
|
2316
|
+
2. Encrypts the token locally into an `enc:v1:` envelope.
|
|
2317
|
+
3. Stores only the ciphertext with `set_secret`, in the component-scoped tier.
|
|
2318
|
+
4. Grants the component permission to resolve it with `grant_secret`.
|
|
2319
|
+
5. Prints the `credentials` reference for the deploy to use.
|
|
2084
2320
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
```bash
|
|
2088
|
-
harper deploy \
|
|
2089
|
-
project=<name> \
|
|
2090
|
-
package=<package> \
|
|
2091
|
-
auth_username=<username> \
|
|
2092
|
-
auth_password=<password> \
|
|
2093
|
-
target=<remote> \
|
|
2094
|
-
restart=true \
|
|
2095
|
-
replicated=true
|
|
2096
|
-
```
|
|
2321
|
+
Use a **fine-grained** personal access token (PAT) scoped to **Contents: Read-only** on the specific repository. Avoid session tokens from `gh` CLI — they typically carry `repo`, `read:org`, `gist`, and `workflow` scopes across your whole account.
|
|
2097
2322
|
|
|
2098
2323
|
#### Examples
|
|
2099
2324
|
|
|
@@ -2103,63 +2328,50 @@ Apply this rule when deploying a Harper application to a remote Harper Fabric cl
|
|
|
2103
2328
|
harper login https://my-cluster.harperdbcloud.com
|
|
2104
2329
|
harper deploy \
|
|
2105
2330
|
project=my-app \
|
|
2106
|
-
package="
|
|
2331
|
+
package="HarperFast/my-app" \
|
|
2107
2332
|
target=https://my-cluster.harperdbcloud.com \
|
|
2108
2333
|
restart=true \
|
|
2109
2334
|
replicated=true
|
|
2110
2335
|
```
|
|
2111
2336
|
|
|
2112
|
-
**
|
|
2337
|
+
**CI/CD deploy using environment variables:**
|
|
2113
2338
|
|
|
2114
2339
|
```bash
|
|
2340
|
+
export HARPER_CLI_USERNAME=admin
|
|
2341
|
+
export HARPER_CLI_PASSWORD=secret
|
|
2115
2342
|
harper deploy \
|
|
2116
2343
|
project=my-app \
|
|
2344
|
+
package="HarperFast/my-app" \
|
|
2117
2345
|
target=https://my-cluster.harperdbcloud.com \
|
|
2118
2346
|
restart=true \
|
|
2119
2347
|
replicated=true
|
|
2120
2348
|
```
|
|
2121
2349
|
|
|
2122
|
-
**Deploy
|
|
2123
|
-
|
|
2124
|
-
```bash
|
|
2125
|
-
harper deploy ref=v1.2.0 restart=true replicated=true
|
|
2126
|
-
```
|
|
2127
|
-
|
|
2128
|
-
**Deploy a private repository by reference with a stored credential:**
|
|
2129
|
-
|
|
2130
|
-
```bash
|
|
2131
|
-
harper deploy by_ref=true credential=true restart=true replicated=true
|
|
2132
|
-
```
|
|
2133
|
-
|
|
2134
|
-
**GitHub Actions — deploy the pull request head commit explicitly:**
|
|
2350
|
+
**Deploy by reference in GitHub Actions (pull request):**
|
|
2135
2351
|
|
|
2136
2352
|
```bash
|
|
2137
2353
|
harper deploy ref=${{ github.event.pull_request.head.sha }} restart=true replicated=true
|
|
2138
2354
|
```
|
|
2139
2355
|
|
|
2140
|
-
**
|
|
2356
|
+
**Deploy a private repo by reference with a provisioned credential:**
|
|
2141
2357
|
|
|
2142
2358
|
```bash
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
target=https://my-cluster.harperdbcloud.com \
|
|
2149
|
-
restart=true \
|
|
2150
|
-
replicated=true
|
|
2359
|
+
# Provision once (run as super_user)
|
|
2360
|
+
harper deploy setup=true
|
|
2361
|
+
|
|
2362
|
+
# Deploy subsequently
|
|
2363
|
+
harper deploy by_ref=true credential=true restart=true replicated=true
|
|
2151
2364
|
```
|
|
2152
2365
|
|
|
2153
2366
|
#### Notes
|
|
2154
2367
|
|
|
2155
|
-
- `
|
|
2156
|
-
-
|
|
2157
|
-
-
|
|
2158
|
-
-
|
|
2159
|
-
-
|
|
2160
|
-
-
|
|
2161
|
-
-
|
|
2162
|
-
- Deploy credentials provisioned via `harper deploy setup=true` are stored scoped to the component and reused on every subsequent deploy, including rollbacks.
|
|
2368
|
+
- `auth_username` and `auth_password` can be passed directly as deploy parameters for one-off commands, but this is not recommended for production. Dedicated authentication parameters take precedence over environment variables and saved login tokens.
|
|
2369
|
+
- The `enc:v1:` envelope means the plaintext token never leaves your machine — only ciphertext is stored and replicated.
|
|
2370
|
+
- Deploy credentials are stored scoped to the component, never in the global `processEnv` tier. If a global secret exists at the derived name, it is converted to the component-scoped tier automatically.
|
|
2371
|
+
- Because stored credentials are durable, later deploys and rollbacks reuse them without re-entering anything.
|
|
2372
|
+
- The unpushed-commit check is skipped under GitHub Actions; the dirty-tree warning still applies.
|
|
2373
|
+
- Deploying by reference means the cluster installs and builds from source. If your application requires a build step that cannot run on the node, deploy the built output as a payload deploy instead.
|
|
2374
|
+
- For SSH-based private repos, use the `add_ssh_key` operation to register keys before deploying.
|
|
2163
2375
|
|
|
2164
2376
|
### 4.2 Creating a Harper Fabric Account and Cluster
|
|
2165
2377
|
|
|
@@ -2598,11 +2810,11 @@ myApp:
|
|
|
2598
2810
|
|
|
2599
2811
|
### 4.7 v5 Upgrade: Breaking Changes and Migration Guide
|
|
2600
2812
|
|
|
2601
|
-
Instructions for the agent to apply
|
|
2813
|
+
Instructions for the agent to apply when migrating a Harper application to v5, covering all breaking changes and required code updates.
|
|
2602
2814
|
|
|
2603
2815
|
#### When to Use
|
|
2604
2816
|
|
|
2605
|
-
Apply this rule
|
|
2817
|
+
Apply this rule when upgrading an existing Harper application to v5, when encountering runtime errors related to renamed packages, changed APIs, or security restrictions after a v5 upgrade, or when scaffolding new v5-compatible application code.
|
|
2606
2818
|
|
|
2607
2819
|
#### How It Works
|
|
2608
2820
|
|
|
@@ -2612,22 +2824,20 @@ Apply this rule whenever upgrading an existing Harper application to v5, encount
|
|
|
2612
2824
|
import { tables } from 'harper';
|
|
2613
2825
|
```
|
|
2614
2826
|
|
|
2615
|
-
2. **Enable `allowInstallScripts` if packages require install scripts**: Harper v5 uses `--ignore-scripts` by default when installing packages. If
|
|
2827
|
+
2. **Enable `allowInstallScripts` if packages require install scripts**: Harper v5 uses `--ignore-scripts` by default when installing packages. If a package requires execution of install scripts (e.g., to install native binaries), set the `allowInstallScripts` option when deploying.
|
|
2616
2828
|
|
|
2617
|
-
3. **Update `Table.get` usage — return value is now a
|
|
2618
|
-
- Replace
|
|
2829
|
+
3. **Update `Table.get` usage — return value is now a frozen record object**: `Table.get` now returns a plain record object, not a table class instance. The record is frozen; you cannot add or mutate properties directly.
|
|
2830
|
+
- Replace direct property mutation:
|
|
2619
2831
|
|
|
2620
2832
|
```javascript
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
if (record.wasLoadedFromSource()) {
|
|
2624
|
-
// record was loaded from origin (not cache)
|
|
2625
|
-
}
|
|
2833
|
+
let record = await Table.get(id);
|
|
2834
|
+
record = { ...record, property: 'changed' };
|
|
2626
2835
|
```
|
|
2627
2836
|
|
|
2837
|
+
- Replace `wasLoadedFromSource()` with `loadedFromSource` on the `target` object:
|
|
2838
|
+
|
|
2628
2839
|
```javascript
|
|
2629
|
-
|
|
2630
|
-
const target = new RequestTarget(); // note that this is passed in if you are overriding the `get` method
|
|
2840
|
+
const target = new RequestTarget();
|
|
2631
2841
|
target.id = id;
|
|
2632
2842
|
const record = await Table.get(target);
|
|
2633
2843
|
if (target.loadedFromSource) {
|
|
@@ -2635,107 +2845,86 @@ Apply this rule whenever upgrading an existing Harper application to v5, encount
|
|
|
2635
2845
|
}
|
|
2636
2846
|
```
|
|
2637
2847
|
|
|
2638
|
-
|
|
2848
|
+
The record objects still expose `getUpdatedTime` and `getExpiresAt` methods.
|
|
2639
2849
|
|
|
2640
|
-
|
|
2850
|
+
4. **Update transaction and context handling using `getContext`**: Harper v5 uses asynchronous context tracking. Context and the current transaction are automatically carried to all downstream calls — you no longer pass context explicitly. Import `getContext` and `transaction` from `harper`:
|
|
2641
2851
|
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
record.property = 'changed';
|
|
2646
|
-
```
|
|
2852
|
+
```javascript
|
|
2853
|
+
import { getContext, transaction } from 'harper';
|
|
2854
|
+
```
|
|
2647
2855
|
|
|
2648
|
-
|
|
2649
|
-
// NEW
|
|
2650
|
-
let record = await Table.get(id);
|
|
2651
|
-
record = { ...record, property: 'changed' };
|
|
2652
|
-
```
|
|
2856
|
+
If your code previously omitted context to escape a transaction (e.g., to poll for updated data), explicitly commit the transaction and/or wrap each read in a new `transaction()` call:
|
|
2653
2857
|
|
|
2654
|
-
|
|
2655
|
-
|
|
2858
|
+
```javascript
|
|
2859
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2860
|
+
import { getContext, transaction } from 'harper';
|
|
2861
|
+
class MyResource {
|
|
2862
|
+
static async get(target) {
|
|
2863
|
+
await getContext().transaction.commit();
|
|
2864
|
+
while ((await transaction(() => Table.get(target))).status !== 'ready') {
|
|
2865
|
+
await delay(100);
|
|
2866
|
+
}
|
|
2867
|
+
return Table.get(target);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
```
|
|
2656
2871
|
|
|
2657
|
-
|
|
2658
|
-
import { getContext, transaction } from 'harper';
|
|
2659
|
-
```
|
|
2872
|
+
5. **Register allowed spawn commands via `allowedSpawnCommands`**: `spawn` and `execFile` may only launch executables listed in `applications.allowedSpawnCommands` in `harperdb-config.yaml`. Only the first token of the command is matched. `exec` is not usable through the substituted module; `execSync` always throws.
|
|
2660
2873
|
|
|
2661
|
-
|
|
2874
|
+
```yaml
|
|
2875
|
+
applications:
|
|
2876
|
+
allowedSpawnCommands:
|
|
2877
|
+
- npm
|
|
2878
|
+
- node
|
|
2879
|
+
```
|
|
2662
2880
|
|
|
2663
|
-
|
|
2664
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
2665
|
-
import { getContext, transaction } from 'harper';
|
|
2666
|
-
class MyResource {
|
|
2667
|
-
static async get(target) {
|
|
2668
|
-
await getContext().transaction.commit();
|
|
2669
|
-
while ((await transaction(() => Table.get(target))).status !== 'ready') {
|
|
2670
|
-
await delay(100);
|
|
2671
|
-
}
|
|
2672
|
-
return Table.get(target);
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
```
|
|
2881
|
+
Additionally, `spawn`, `execFile`, and `fork` now require a `name` property in the `options` argument to prevent process multiplication across threads.
|
|
2676
2882
|
|
|
2677
|
-
|
|
2883
|
+
6. **Use `saveBeforeCommit` instead of `blob.save()`**: The `blob.save()` method has been removed. Pass the `saveBeforeCommit` flag in the options to the `Blob` constructor instead.
|
|
2678
2884
|
|
|
2679
|
-
|
|
2885
|
+
7. **Handle `headers` on returned response objects**: If you return an object from a REST method with a `headers` property, Harper v5 will use it as the response headers.
|
|
2680
2886
|
|
|
2681
|
-
|
|
2887
|
+
8. **Configure the VM module loader and `lockdown` in `harperdb-config.yaml`**: v5 loads application modules through Node.js's VM module API. Control all behavior under the `applications` key:
|
|
2682
2888
|
|
|
2683
2889
|
```yaml
|
|
2684
2890
|
applications:
|
|
2685
|
-
lockdown: freeze-after-load
|
|
2686
|
-
moduleLoader: vm-current-context
|
|
2687
|
-
dependencyLoader: auto
|
|
2688
|
-
allowedDirectory: app
|
|
2689
|
-
allowedSpawnCommands:
|
|
2891
|
+
lockdown: freeze-after-load
|
|
2892
|
+
moduleLoader: vm-current-context
|
|
2893
|
+
dependencyLoader: auto
|
|
2894
|
+
allowedDirectory: app
|
|
2895
|
+
allowedSpawnCommands:
|
|
2690
2896
|
- npm
|
|
2691
2897
|
- node
|
|
2692
|
-
# allowedBuiltinModules: [] # if omitted, all Node.js built-ins are allowed
|
|
2693
2898
|
```
|
|
2694
2899
|
|
|
2695
2900
|
**`moduleLoader` options:**
|
|
2696
2901
|
|
|
2697
2902
|
| Value | Behavior |
|
|
2698
2903
|
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
2699
|
-
| `vm-current-context` | Default. VM loader in Harper's own context; shares intrinsics with Harper
|
|
2700
|
-
| `vm` | VM loader in a separate per-application context with its own intrinsics
|
|
2701
|
-
| `native` | Standard Node.js `import()
|
|
2702
|
-
| `compartment` | SES Compartment-based loading
|
|
2904
|
+
| `vm-current-context` | Default. VM loader in Harper's own context; shares intrinsics with Harper. Best compatibility. |
|
|
2905
|
+
| `vm` | VM loader in a separate per-application context with its own intrinsics. Stronger isolation but may cause `instanceof` failures. |
|
|
2906
|
+
| `native` | Standard Node.js `import()`. No VM loader; application-specific context (`logger`, `config`) unavailable. |
|
|
2907
|
+
| `compartment` | SES Compartment-based loading. For specialized sandboxing only. |
|
|
2703
2908
|
|
|
2704
2909
|
**`lockdown` options:**
|
|
2705
2910
|
|
|
2706
|
-
| Value | Behavior
|
|
2707
|
-
| ------------------- |
|
|
2708
|
-
| `freeze-after-load` | Default. Freezes intrinsics after all components
|
|
2709
|
-
| `freeze` | Freezes intrinsics before loading any application code.
|
|
2710
|
-
| `ses` | Full SES lockdown via the `ses` package
|
|
2711
|
-
| `none` | No lockdown. Use as a temporary workaround only.
|
|
2712
|
-
- To disable the VM loader entirely and restore pre-v5 behavior:
|
|
2713
|
-
|
|
2714
|
-
```yaml
|
|
2715
|
-
applications:
|
|
2716
|
-
moduleLoader: native
|
|
2717
|
-
```
|
|
2718
|
-
|
|
2719
|
-
- To restrict which Node.js built-ins are accessible:
|
|
2911
|
+
| Value | Behavior |
|
|
2912
|
+
| ------------------- | ------------------------------------------------------- |
|
|
2913
|
+
| `freeze-after-load` | Default. Freezes intrinsics after all components load. |
|
|
2914
|
+
| `freeze` | Freezes intrinsics before loading any application code. |
|
|
2915
|
+
| `ses` | Full SES lockdown via the `ses` package. Strictest. |
|
|
2916
|
+
| `none` | No lockdown. Use as a temporary workaround only. |
|
|
2720
2917
|
|
|
2721
|
-
|
|
2722
|
-
applications:
|
|
2723
|
-
allowedBuiltinModules:
|
|
2724
|
-
- fs
|
|
2725
|
-
- path
|
|
2726
|
-
- http
|
|
2727
|
-
```
|
|
2728
|
-
|
|
2729
|
-
- To allow loading modules from outside the application directory:
|
|
2918
|
+
To disable the VM loader entirely and restore pre-v5 behavior:
|
|
2730
2919
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2920
|
+
```yaml
|
|
2921
|
+
applications:
|
|
2922
|
+
moduleLoader: native
|
|
2923
|
+
```
|
|
2735
2924
|
|
|
2736
2925
|
#### Examples
|
|
2737
2926
|
|
|
2738
|
-
**Full transaction
|
|
2927
|
+
**Full transaction polling pattern (v5):**
|
|
2739
2928
|
|
|
2740
2929
|
```javascript
|
|
2741
2930
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
@@ -2743,9 +2932,7 @@ import { getContext, transaction } from 'harper';
|
|
|
2743
2932
|
|
|
2744
2933
|
class MyResource {
|
|
2745
2934
|
static async get(target) {
|
|
2746
|
-
// Explicitly commit the transaction to see updated data
|
|
2747
2935
|
await getContext().transaction.commit();
|
|
2748
|
-
// Start a new transaction for each get to see the latest data
|
|
2749
2936
|
while ((await transaction(() => Table.get(target))).status !== 'ready') {
|
|
2750
2937
|
await delay(100);
|
|
2751
2938
|
}
|
|
@@ -2754,11 +2941,9 @@ class MyResource {
|
|
|
2754
2941
|
}
|
|
2755
2942
|
```
|
|
2756
2943
|
|
|
2757
|
-
|
|
2944
|
+
**Checking `loadedFromSource` after `Table.get`:**
|
|
2758
2945
|
|
|
2759
2946
|
```javascript
|
|
2760
|
-
import { tables } from 'harper';
|
|
2761
|
-
|
|
2762
2947
|
const target = new RequestTarget();
|
|
2763
2948
|
target.id = id;
|
|
2764
2949
|
const record = await Table.get(target);
|
|
@@ -2780,14 +2965,27 @@ applications:
|
|
|
2780
2965
|
- node
|
|
2781
2966
|
```
|
|
2782
2967
|
|
|
2968
|
+
**Restricting allowed built-in modules:**
|
|
2969
|
+
|
|
2970
|
+
```yaml
|
|
2971
|
+
applications:
|
|
2972
|
+
allowedBuiltinModules:
|
|
2973
|
+
- fs
|
|
2974
|
+
- path
|
|
2975
|
+
- http
|
|
2976
|
+
```
|
|
2977
|
+
|
|
2783
2978
|
#### Notes
|
|
2784
2979
|
|
|
2785
|
-
-
|
|
2786
|
-
-
|
|
2787
|
-
-
|
|
2788
|
-
- `
|
|
2789
|
-
-
|
|
2790
|
-
-
|
|
2980
|
+
- Always import Harper APIs from `'harper'`, not from global variables or `'harperdb'`.
|
|
2981
|
+
- `getContext` is exported from `'harper'` and provides access to the current transaction without passing context explicitly.
|
|
2982
|
+
- Record objects returned by `Table.get` are frozen — spread into a new object before modifying.
|
|
2983
|
+
- `loadedFromSource` is a property on the `target` object, replacing the removed `wasLoadedFromSource()` instance method.
|
|
2984
|
+
- `saveBeforeCommit` replaces the removed `blob.save()` method.
|
|
2985
|
+
- The `headers` property on a returned REST response object is used as response headers.
|
|
2986
|
+
- Under `lockdown: ses`, the constrained `fetch` applies only in `vm` mode. In `vm-current-context` and `native` modes, application code uses the standard global `fetch`.
|
|
2987
|
+
- In production, `allowedDirectory: app` is the default; modules outside the application directory tree will throw. Set `allowedDirectory: any` only if legitimately required.
|
|
2988
|
+
- `dependencyLoader: native` is a narrower option than `moduleLoader: native` — it uses native loading only for npm packages while keeping the VM loader for first-party application source files.
|
|
2791
2989
|
|
|
2792
2990
|
### 4.8 Delegating to the Built-in Agent
|
|
2793
2991
|
|