@harperfast/template-react-ts-ssr-studio 1.13.3 → 1.13.5
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 +687 -453
- package/.agents/skills/harper-best-practices/SKILL.md +24 -21
- package/.agents/skills/harper-best-practices/rules/adding-tables-with-schemas.md +2 -0
- package/.agents/skills/harper-best-practices/rules/automatic-apis.md +2 -0
- package/.agents/skills/harper-best-practices/rules/caching.md +2 -0
- package/.agents/skills/harper-best-practices/rules/checking-authentication.md +2 -0
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +2 -0
- package/.agents/skills/harper-best-practices/rules/defining-relationships.md +102 -40
- package/.agents/skills/harper-best-practices/rules/deploying-from-ci.md +121 -93
- package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +149 -103
- package/.agents/skills/harper-best-practices/rules/extending-tables.md +2 -0
- package/.agents/skills/harper-best-practices/rules/handling-binary-data.md +2 -0
- package/.agents/skills/harper-best-practices/rules/load-env.md +2 -0
- package/.agents/skills/harper-best-practices/rules/logging.md +2 -0
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +124 -91
- package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +86 -82
- package/.agents/skills/harper-best-practices/rules/real-time-apps.md +2 -0
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +92 -59
- package/.agents/skills/harper-best-practices/rules/serving-web-content.md +2 -0
- package/.agents/skills/harper-best-practices/rules/typescript-type-stripping.md +2 -0
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +2 -0
- package/.agents/skills/harper-best-practices/rules/vector-indexing.md +2 -0
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +44 -0
- package/agent/skills/harper-best-practices/AGENTS.md +687 -453
- package/agent/skills/harper-best-practices/SKILL.md +26 -21
- package/agent/skills/harper-best-practices/rules/adding-tables-with-schemas.md +2 -0
- package/agent/skills/harper-best-practices/rules/automatic-apis.md +2 -0
- package/agent/skills/harper-best-practices/rules/caching.md +2 -0
- package/agent/skills/harper-best-practices/rules/checking-authentication.md +2 -0
- package/agent/skills/harper-best-practices/rules/custom-resources.md +2 -0
- package/agent/skills/harper-best-practices/rules/defining-relationships.md +102 -40
- package/agent/skills/harper-best-practices/rules/deploying-from-ci.md +121 -93
- package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +149 -103
- package/agent/skills/harper-best-practices/rules/extending-tables.md +2 -0
- package/agent/skills/harper-best-practices/rules/handling-binary-data.md +2 -0
- package/agent/skills/harper-best-practices/rules/load-env.md +2 -0
- package/agent/skills/harper-best-practices/rules/logging.md +2 -0
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +124 -91
- package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +86 -82
- package/agent/skills/harper-best-practices/rules/real-time-apps.md +2 -0
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +92 -59
- package/agent/skills/harper-best-practices/rules/serving-web-content.md +2 -0
- package/agent/skills/harper-best-practices/rules/typescript-type-stripping.md +2 -0
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +2 -0
- package/agent/skills/harper-best-practices/rules/vector-indexing.md +2 -0
- package/agent/skills/harper-best-practices/rules.manifest.yaml +44 -0
- package/package.json +1 -1
- package/skills-lock.json +1 -1
|
@@ -8,6 +8,8 @@ Guidelines for building scalable, secure, and performant applications on Harper.
|
|
|
8
8
|
|
|
9
9
|
Instructions for the agent to follow when adding tables to a Harper database.
|
|
10
10
|
|
|
11
|
+
**Instead of:** Postgres, MySQL, MongoDB, DynamoDB, Supabase, Firebase. Use this rule rather than adding that service. See `harper-architecture`.
|
|
12
|
+
|
|
11
13
|
#### When to Use
|
|
12
14
|
|
|
13
15
|
Use this skill when you need to define new data structures or modify existing ones in a Harper database.
|
|
@@ -40,26 +42,26 @@ type ExamplePerson @table @export {
|
|
|
40
42
|
}
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
### 1.2 Schema Design Tooling
|
|
45
|
+
### 1.2 Schema Design and GraphQL Tooling
|
|
44
46
|
|
|
45
47
|
Instructions for the agent to follow when designing Harper schemas, applying core directives, and configuring GraphQL tooling.
|
|
46
48
|
|
|
47
49
|
#### When to Use
|
|
48
50
|
|
|
49
|
-
Apply this rule when creating or modifying Harper schema files
|
|
51
|
+
Apply this rule when creating or modifying Harper schema files, configuring `graphqlSchema` 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
52
|
|
|
51
53
|
#### How It Works
|
|
52
54
|
|
|
53
|
-
1. **
|
|
55
|
+
1. **Load the schema via `graphqlSchema`**: In the component's `config.yaml`, point to the schema file:
|
|
54
56
|
|
|
55
57
|
```yaml
|
|
56
58
|
graphqlSchema:
|
|
57
59
|
files: 'schema.graphql'
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
Both plugins and applications can specify schemas.
|
|
62
|
+
Both plugins and applications can specify schemas. The schema ensures required tables exist on deployment, enforces types, controls indexing, and defines relationships.
|
|
61
63
|
|
|
62
|
-
2. **Mark
|
|
64
|
+
2. **Mark tables with `@table`**: Apply `@table` to every GraphQL type that should become a Harper database table. The type name becomes the table name by default.
|
|
63
65
|
|
|
64
66
|
```graphql
|
|
65
67
|
type Dog @table {
|
|
@@ -70,47 +72,60 @@ Apply this rule when creating or modifying Harper schema files (`.graphql`), con
|
|
|
70
72
|
}
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
- `String` or `ID` → UUID string
|
|
75
|
-
- `Int`, `Long`, or `Any` → auto-incrementing integer
|
|
75
|
+
Use `@table` arguments to override defaults:
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
| Argument | Type | Default | Description |
|
|
78
|
+
| ------------------------ | --------- | ----------------------------- | ------------------------------------------------------------------------------ |
|
|
79
|
+
| `table` | `String` | type name | Override the table name |
|
|
80
|
+
| `database` | `String` | `"data"` | Database to place the table in |
|
|
81
|
+
| `expiration` | `Int` | — | Seconds until a record goes stale |
|
|
82
|
+
| `eviction` | `Int` | `0` | Additional seconds after `expiration` before physical removal |
|
|
83
|
+
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
|
|
84
|
+
| `replicate` | `Boolean` | `true` | Enable replication of this table |
|
|
85
|
+
| `replicaExpiration` | `Float` | `3600` | Seconds a record fetched from another node is kept as a local replica |
|
|
86
|
+
| `replicaRefreshFraction` | `Float` | `0.5` | Fraction of `replicaExpiration` after which a read renews the replica lifetime |
|
|
87
|
+
| `audit` | `Boolean` | `logging.auditLog` | Enable the table's transaction log |
|
|
88
|
+
| `cacheControl` | `String` | — | `Cache-Control` header for anonymous GET/HEAD 200/304 responses |
|
|
89
|
+
| `randomAccessFields` | `Boolean` | `storage.randomAccessFields` | Pin this table's record encoding |
|
|
78
90
|
|
|
79
|
-
|
|
91
|
+
3. **Designate a primary key with `@primaryKey`**: Apply `@primaryKey` to exactly one field per table. Primary keys must be unique; duplicate inserts are rejected.
|
|
80
92
|
|
|
81
93
|
```graphql
|
|
82
|
-
type
|
|
94
|
+
type Product @table {
|
|
83
95
|
id: Long @primaryKey
|
|
84
|
-
name: String
|
|
96
|
+
name: String
|
|
85
97
|
}
|
|
86
98
|
```
|
|
87
99
|
|
|
88
|
-
If
|
|
100
|
+
If no primary key is provided on insert, Harper auto-generates one:
|
|
101
|
+
- **UUID string** — when type is `String` or `ID`
|
|
102
|
+
- **Auto-incrementing integer** — when type is `Int`, `Long`, or `Any`
|
|
103
|
+
|
|
104
|
+
Prefer `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
|
|
89
105
|
|
|
90
|
-
|
|
106
|
+
4. **Index queryable fields with `@indexed`**: Apply `@indexed` to any field that must be filterable via REST queries, SQL, or NoSQL operations.
|
|
91
107
|
|
|
92
108
|
```graphql
|
|
93
|
-
type
|
|
109
|
+
type Product @table {
|
|
94
110
|
id: Long @primaryKey
|
|
111
|
+
category: String @indexed
|
|
112
|
+
price: Float @indexed
|
|
95
113
|
}
|
|
96
114
|
```
|
|
97
115
|
|
|
98
|
-
|
|
116
|
+
If the field value is an array, each element is individually indexed. Null values are indexed by default.
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
5. **Expose tables externally with `@export`**: Apply `@export` to make a table accessible via REST, MQTT, and other interfaces. The optional `name` parameter sets the URL path segment.
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
| `cacheControl` | `String` | — | `Cache-Control` header on anonymous GET/HEAD 200/304 responses |
|
|
111
|
-
| `randomAccessFields` | `Boolean` | `storage.randomAccessFields` | Pin this table's record encoding |
|
|
120
|
+
```graphql
|
|
121
|
+
type MyTable @table @export(name: "my-table") {
|
|
122
|
+
id: Long @primaryKey
|
|
123
|
+
}
|
|
124
|
+
```
|
|
112
125
|
|
|
113
|
-
|
|
126
|
+
`@export` alone does not serve HTTP traffic — REST must also be enabled for the application via `rest: true` in `config.yaml` or Harper's built-in default. `@export` is a routing directive, not access control: omitting it returns 404 to callers but the table remains accessible through the Operations API and SQL.
|
|
127
|
+
|
|
128
|
+
6. **Prevent extra properties with `@sealed`** (when needed): Apply `@sealed` to reject records that include properties beyond those declared in the schema. By default, Harper allows additional properties.
|
|
114
129
|
|
|
115
130
|
```graphql
|
|
116
131
|
type StrictRecord @table @sealed {
|
|
@@ -119,13 +134,33 @@ Apply this rule when creating or modifying Harper schema files (`.graphql`), con
|
|
|
119
134
|
}
|
|
120
135
|
```
|
|
121
136
|
|
|
122
|
-
|
|
123
|
-
- `@createdTime` — assigns Unix epoch milliseconds on record creation
|
|
124
|
-
- `@updatedTime` — assigns Unix epoch milliseconds on each update
|
|
125
|
-
- `@expiresAt` — marks a field as the record's absolute expiration time (Unix epoch milliseconds); authoritative over the table-level `expiration` default
|
|
137
|
+
7. **Use unique database names for plugins and applications**: All tables default to the `data` database. Specify a distinct `database` argument on `@table` to avoid naming collisions between components.
|
|
126
138
|
|
|
127
|
-
|
|
139
|
+
8. **Apply timestamp and expiration field directives** where needed:
|
|
140
|
+
- `@createdTime` — auto-assigns a Unix epoch millisecond timestamp on record creation.
|
|
141
|
+
- `@updatedTime` — auto-assigns a Unix epoch millisecond timestamp on each update.
|
|
142
|
+
- `@expiresAt` — marks a field as the record's absolute expiration time (Unix epoch milliseconds); overrides the table-level `expiration` default.
|
|
143
|
+
- `@hidden` — suppresses the type or field from MCP tool descriptors and the OpenAPI document. Does not restrict data access.
|
|
128
144
|
|
|
145
|
+
```graphql
|
|
146
|
+
type Event @table {
|
|
147
|
+
id: Long @primaryKey
|
|
148
|
+
createdAt: Long @createdTime
|
|
149
|
+
updatedAt: Long @updatedTime
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type Session @table {
|
|
153
|
+
id: ID @primaryKey
|
|
154
|
+
token: String
|
|
155
|
+
expiresAt: Long @expiresAt
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Key `@expiresAt` behaviors:
|
|
160
|
+
- Takes an absolute Unix epoch millisecond timestamp, not a duration. A full-record `put` that omits the field clears it; a `patch` preserves it.
|
|
161
|
+
- A per-record value may extend or shorten a record's expiration relative to the table-level `expiration` default. When a record omits the field, the table default (if any) applies.
|
|
162
|
+
|
|
163
|
+
9. **Use `@embed`** to automatically compute an embedding vector for an attribute whenever the source field is written (requires `source` and `model` arguments; field type must be `[Float]`):
|
|
129
164
|
```graphql
|
|
130
165
|
type Document @table {
|
|
131
166
|
id: Long @primaryKey
|
|
@@ -133,12 +168,11 @@ Apply this rule when creating or modifying Harper schema files (`.graphql`), con
|
|
|
133
168
|
embedding: [Float] @embed(source: "text", model: "default")
|
|
134
169
|
}
|
|
135
170
|
```
|
|
136
|
-
|
|
137
|
-
10. **Use `@hidden`** on types or fields to suppress them from MCP tool descriptors and the OpenAPI document. This is a metadata-visibility directive only — it does not restrict data access. Use `attribute_permissions` on roles for field-level access control.
|
|
171
|
+
The attribute is automatically indexed with an HNSW vector index. A failure to compute the embedding fails the write. An update that does not touch the `source` field leaves the vector unchanged. Setting the `source` field to `null` sets the vector to `null`.
|
|
138
172
|
|
|
139
173
|
#### Examples
|
|
140
174
|
|
|
141
|
-
**Minimal two
|
|
175
|
+
**Minimal schema with two tables:**
|
|
142
176
|
|
|
143
177
|
```graphql
|
|
144
178
|
type Dog @table {
|
|
@@ -154,7 +188,7 @@ type Breed @table {
|
|
|
154
188
|
}
|
|
155
189
|
```
|
|
156
190
|
|
|
157
|
-
**
|
|
191
|
+
**Caching table with expiration, eviction, and scan tuning:**
|
|
158
192
|
|
|
159
193
|
```graphql
|
|
160
194
|
# Expire after 5 minutes, evict after 1 hour, scan every 10 minutes
|
|
@@ -174,7 +208,7 @@ type Product @table(cacheControl: "public, max-age=60") @export {
|
|
|
174
208
|
}
|
|
175
209
|
```
|
|
176
210
|
|
|
177
|
-
**Table
|
|
211
|
+
**Table in a named database with expiration:**
|
|
178
212
|
|
|
179
213
|
```graphql
|
|
180
214
|
type Event @table(database: "analytics", expiration: 86400) {
|
|
@@ -183,29 +217,28 @@ type Event @table(database: "analytics", expiration: 86400) {
|
|
|
183
217
|
}
|
|
184
218
|
```
|
|
185
219
|
|
|
186
|
-
**
|
|
220
|
+
**Replica lifetime tuning:**
|
|
187
221
|
|
|
188
222
|
```graphql
|
|
189
|
-
|
|
223
|
+
# Keep fetched records for a day; a read after six hours renews them
|
|
224
|
+
type Order @table(replicaExpiration: 86400, replicaRefreshFraction: 0.25) {
|
|
190
225
|
id: ID @primaryKey
|
|
191
|
-
|
|
192
|
-
expiresAt: Long @expiresAt
|
|
226
|
+
region: String @indexed
|
|
193
227
|
}
|
|
194
228
|
```
|
|
195
229
|
|
|
196
|
-
**
|
|
230
|
+
**Random-access field encoding pinned on the table:**
|
|
197
231
|
|
|
198
232
|
```graphql
|
|
199
|
-
type
|
|
200
|
-
id:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
updatedAt: Long @updatedTime
|
|
233
|
+
type Reading @table(randomAccessFields: true) {
|
|
234
|
+
id: ID @primaryKey
|
|
235
|
+
sensorId: String @indexed
|
|
236
|
+
celsius: Float
|
|
237
|
+
recordedAt: Int
|
|
205
238
|
}
|
|
206
239
|
```
|
|
207
240
|
|
|
208
|
-
**`config.yaml` schema
|
|
241
|
+
**`config.yaml` loading the schema:**
|
|
209
242
|
|
|
210
243
|
```yaml
|
|
211
244
|
graphqlSchema:
|
|
@@ -214,25 +247,29 @@ graphqlSchema:
|
|
|
214
247
|
|
|
215
248
|
#### Notes
|
|
216
249
|
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
- `cacheControl` emits the header
|
|
220
|
-
-
|
|
221
|
-
- Eviction removes non-indexed record data but does not remove a record from its secondary indexes; indexes remain functional for evicted records
|
|
222
|
-
- `
|
|
223
|
-
-
|
|
250
|
+
- Schemas are flexible by default — records may include additional properties. Use `@sealed` to prevent this.
|
|
251
|
+
- `@export` is a routing directive, not an access control mechanism. Use RBAC and `attribute_permissions` to restrict data access.
|
|
252
|
+
- `cacheControl` on `@table` only emits the header for anonymous (unauthenticated) GET/HEAD 200/304 responses. Authenticated responses always receive `Cache-Control: private, no-cache`.
|
|
253
|
+
- `expiration`, `eviction`, and `scanInterval` work together: `expiration` marks a record stale, `eviction` controls physical removal, and `scanInterval` sets how often the sweep runs. Eviction scans are clock-aligned to the server's local timezone, not startup-aligned.
|
|
254
|
+
- Eviction removes non-indexed record data but does not remove a record from its secondary indexes; indexes remain functional for evicted records.
|
|
255
|
+
- `replicaExpiration` and `replicaRefreshFraction` apply only on the RocksDB engine. On LMDB, remote reads remain proxy reads regardless of these settings.
|
|
256
|
+
- Disabling replication on a table (`replicate: false`) and re-enabling it later will not catch up on writes made while replication was disabled.
|
|
257
|
+
- `randomAccessFields` on `@table` pins the encoding when the table is created. Editing the argument later does not repin an existing table.
|
|
258
|
+
- `@hidden` is a metadata-visibility directive only — it does not restrict data access. Use `attribute_permissions` on roles for field-level access control.
|
|
224
259
|
|
|
225
260
|
### 1.3 Defining Relationships Between Tables in Harper
|
|
226
261
|
|
|
227
|
-
Instructions for the agent to
|
|
262
|
+
Instructions for the agent to define and query relationships between Harper tables using the `@relationship` directive in GraphQL schemas.
|
|
263
|
+
|
|
264
|
+
**Instead of:** ORM relations and join tables (Prisma, Drizzle, Sequelize). Use this rule rather than adding that service. See `harper-architecture`.
|
|
228
265
|
|
|
229
266
|
#### When to Use
|
|
230
267
|
|
|
231
|
-
Apply this rule when
|
|
268
|
+
Apply this rule when writing Harper schemas that require join queries across tables, or when query results must include related records as nested properties. Use it whenever a foreign key exists in one table that references another, or when modeling one-to-many, many-to-one, or many-to-many associations.
|
|
232
269
|
|
|
233
270
|
#### How It Works
|
|
234
271
|
|
|
235
|
-
1. **
|
|
272
|
+
1. **Declare a many-to-one relationship with `from`**: Place `@relationship(from: attribute)` on a field when the foreign key lives in the current table and references the primary key of the target table. The result type is a single object.
|
|
236
273
|
|
|
237
274
|
```graphql
|
|
238
275
|
type RealityShow @table @export {
|
|
@@ -248,17 +285,7 @@ Apply this rule when adding foreign key relationships between schema tables, ena
|
|
|
248
285
|
}
|
|
249
286
|
```
|
|
250
287
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
```graphql
|
|
254
|
-
type RealityShow @table @export {
|
|
255
|
-
id: Long @primaryKey
|
|
256
|
-
networkIds: [Long] @indexed
|
|
257
|
-
networks: [Network] @relationship(from: networkIds)
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
2. **Use `@relationship(to: attribute)` for one-to-many or many-to-many**: Place this on the table whose primary key is referenced by the foreign key in the target table. The `to` parameter names the attribute on the target table that holds the foreign key. The result type **must** be an array.
|
|
288
|
+
2. **Declare a one-to-many relationship with `to`**: Place `@relationship(to: attribute)` on a field when the foreign key lives in the target table and references the primary key of the current table. The result type **must** be an array.
|
|
262
289
|
|
|
263
290
|
```graphql
|
|
264
291
|
type Network @table @export {
|
|
@@ -268,14 +295,14 @@ Apply this rule when adding foreign key relationships between schema tables, ena
|
|
|
268
295
|
}
|
|
269
296
|
```
|
|
270
297
|
|
|
271
|
-
3. **
|
|
298
|
+
3. **Declare a foreign-key-to-foreign-key relationship with `@relationship(from: attribute, to: attribute)`**: Specify both `from` and `to` when neither side uses the primary key. Harper resolves the relationship by searching the target table's `to` attribute for matches using the current record's `from` attribute value. The result type must be an array.
|
|
272
299
|
|
|
273
300
|
```graphql
|
|
274
301
|
type OrderItem @table @export {
|
|
275
302
|
id: Long @primaryKey
|
|
276
303
|
orderId: Long @indexed
|
|
277
304
|
productSku: Long @indexed
|
|
278
|
-
products: [Product] @relationship(from: productSku, to: sku)
|
|
305
|
+
products: [Product] @relationship(from: productSku, to: sku) # matches products by sku, not primary key
|
|
279
306
|
}
|
|
280
307
|
|
|
281
308
|
type Product @table @export {
|
|
@@ -285,14 +312,25 @@ Apply this rule when adding foreign key relationships between schema tables, ena
|
|
|
285
312
|
}
|
|
286
313
|
```
|
|
287
314
|
|
|
288
|
-
4. **
|
|
315
|
+
4. **Declare a many-to-many relationship using an array foreign key with `from`**: When the foreign key field is an array, `@relationship(from: attribute)` establishes a many-to-many relationship. The result type must be an array.
|
|
316
|
+
|
|
317
|
+
```graphql
|
|
318
|
+
type RealityShow @table @export {
|
|
319
|
+
id: Long @primaryKey
|
|
320
|
+
networkIds: [Long] @indexed
|
|
321
|
+
networks: [Network] @relationship(from: networkIds)
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
5. **Query across relationships using dot-syntax**: Filter records by related table attributes using dot-chained attribute names. This behaves as an INNER JOIN — only parents with a qualifying related record are returned.
|
|
289
326
|
|
|
290
327
|
```
|
|
328
|
+
GET /RealityShow?network.name=Bravo
|
|
291
329
|
GET /Product/?brand.name=Microsoft
|
|
292
330
|
GET /Brand/?products.name=Keyboard
|
|
293
331
|
```
|
|
294
332
|
|
|
295
|
-
|
|
333
|
+
6. **Include relationship fields in results using `select()`**: Relationship attributes are not included by default. Use `select()` to project them, optionally with a nested field list using `{}`.
|
|
296
334
|
|
|
297
335
|
```
|
|
298
336
|
GET /Product/?brand.name=Microsoft&select(name,brand)
|
|
@@ -300,24 +338,27 @@ Apply this rule when adding foreign key relationships between schema tables, ena
|
|
|
300
338
|
GET /Product/?name=Keyboard&select(name,brand{name,id})
|
|
301
339
|
```
|
|
302
340
|
|
|
303
|
-
|
|
341
|
+
Selecting a relationship without an outer dot-syntax filter on it behaves as a LEFT JOIN: parents remain even when there are no related records.
|
|
304
342
|
|
|
305
|
-
|
|
343
|
+
7. **Filter and page nested relationship results using `[]` inside `select()`**: Apply conditions, sort, and pagination to each parent's related records by placing them inside square brackets in the `select()` expression.
|
|
306
344
|
|
|
307
|
-
```graphql
|
|
308
|
-
type Product @table @export {
|
|
309
|
-
id: Long @primaryKey
|
|
310
|
-
name: String
|
|
311
|
-
resellerIds: [Long] @indexed
|
|
312
|
-
resellers: [Reseller] @relationship(from: "resellerIds")
|
|
313
|
-
}
|
|
314
345
|
```
|
|
346
|
+
GET /Brand/?select(id,name,products[price=ge=number:30&sort(price)&limit(1)&select(id,name,price)])
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
An outer relationship filter (e.g., `products.price=ge=number:30`) removes parents without a qualifying child **and** narrows the nested candidates. A filter inside `products[...]` only narrows projected children without removing their parent.
|
|
315
350
|
|
|
316
|
-
|
|
351
|
+
| Nested option | Behavior |
|
|
352
|
+
| ------------------ | -------------------------------------------------------------------------- |
|
|
353
|
+
| Conditions | Qualify children; combine with `&` (AND), `\|` (OR), and groups. |
|
|
354
|
+
| `sort(price)` | Order eligible children before pagination. |
|
|
355
|
+
| `limit(end)` | Return at most `end` children per parent. |
|
|
356
|
+
| `limit(start,end)` | Skip `start` eligible children and return at most `end - start`. |
|
|
357
|
+
| `select(id,price)` | Project fields from returned children; omit to return whole child records. |
|
|
317
358
|
|
|
318
359
|
#### Examples
|
|
319
360
|
|
|
320
|
-
|
|
361
|
+
##### Schema with bidirectional relationships
|
|
321
362
|
|
|
322
363
|
```graphql
|
|
323
364
|
type Product @table @export {
|
|
@@ -334,30 +375,88 @@ type Brand @table @export {
|
|
|
334
375
|
}
|
|
335
376
|
```
|
|
336
377
|
|
|
337
|
-
|
|
378
|
+
##### Many-to-many without a junction table
|
|
338
379
|
|
|
380
|
+
```graphql
|
|
381
|
+
type Product @table @export {
|
|
382
|
+
id: Long @primaryKey
|
|
383
|
+
name: String
|
|
384
|
+
resellerIds: [Long] @indexed
|
|
385
|
+
resellers: [Reseller] @relationship(from: "resellerIds")
|
|
386
|
+
}
|
|
339
387
|
```
|
|
340
|
-
|
|
341
|
-
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The array order of `resellerIds` is preserved when resolving the relationship.
|
|
394
|
+
|
|
395
|
+
##### Nested filtering and pagination scenarios
|
|
396
|
+
|
|
397
|
+
Schema for the following examples:
|
|
398
|
+
|
|
399
|
+
```graphql
|
|
400
|
+
type Brand @table @export {
|
|
401
|
+
id: Int @primaryKey
|
|
402
|
+
name: String @indexed
|
|
403
|
+
products: [Product] @relationship(to: "brandId")
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
type Product @table @export {
|
|
407
|
+
id: ID @primaryKey
|
|
408
|
+
brandId: Int @indexed
|
|
409
|
+
name: String
|
|
410
|
+
price: Int @indexed
|
|
411
|
+
}
|
|
342
412
|
```
|
|
343
413
|
|
|
344
|
-
**
|
|
414
|
+
**Keep every brand; show its cheapest product costing at least 30** (LEFT JOIN behavior — Budget Basics returns with an empty product list):
|
|
345
415
|
|
|
346
416
|
```
|
|
347
|
-
GET /
|
|
417
|
+
GET /Brand/?select(id,name,products[price=ge=number:30&sort(price)&limit(1)&select(id,name,price)])
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Return only qualifying brands; inherit the outer price filter** (INNER JOIN behavior — Budget Basics is removed):
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
GET /Brand/?products.price=ge=number:30&select(id,name,products[sort(price)&limit(1)&select(id,name,price)])
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Page through each brand's qualifying products** (offset applies independently per brand):
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
GET /Brand/?products.price=ge=number:30&select(id,name,products[sort(price)&limit(1,2)&select(id,name,price)])
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Bound parent pages with stable sorting**:
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
GET /Brand/?products.price=ge=number:30&sort(name,id)&limit(0,20)&select(id,name,products[sort(price,id)&limit(1)&select(id,name,price)])
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Combine OR filters with child sorting and pagination**:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
GET /Brand/?sort(name,id)&limit(0,20)&select(id,name,products[[price=lt=number:30|price=ge=number:60]&sort(-price,id)&limit(1,2)&select(id,name,price)])
|
|
348
442
|
```
|
|
349
443
|
|
|
350
444
|
#### Notes
|
|
351
445
|
|
|
352
|
-
-
|
|
353
|
-
-
|
|
354
|
-
-
|
|
355
|
-
-
|
|
446
|
+
- Index all foreign key fields (e.g., `brandId`, `price`) and outer filter fields to support efficient join queries.
|
|
447
|
+
- Use a unique field as the final sort key (e.g., `sort(price,id)`) to ensure deterministic ordering when values are equal.
|
|
448
|
+
- A nested `limit()` bounds children per parent, not the number of parents. Apply a separate outer `limit()` to bound the parent result set.
|
|
449
|
+
- Nested filters require a table-backed relationship. Filters on plain JSON projections or full-text predicates inside a relationship selection return HTTP 400.
|
|
450
|
+
- Invalid `limit()` bounds (negative or fractional values) return HTTP 400. `limit(0)` returns no children without reading them.
|
|
451
|
+
- A condition traversing another relationship qualifies the immediate child only; it does not narrow that child's projected descendants. Place grandchild constraints in the grandchild's own bracket selection.
|
|
452
|
+
- Schemas can define self-referential relationships to enable parent-child hierarchies within a single table.
|
|
356
453
|
|
|
357
454
|
### 1.4 Vector Indexing
|
|
358
455
|
|
|
359
456
|
Instructions for the agent to enable HNSW vector indexes on table fields and query them for similarity search in Harper.
|
|
360
457
|
|
|
458
|
+
**Instead of:** Pinecone, Weaviate, pgvector. Use this rule rather than adding that service. See `harper-architecture`.
|
|
459
|
+
|
|
361
460
|
#### When to Use
|
|
362
461
|
|
|
363
462
|
Apply this rule when adding a vector similarity search capability to a Harper table — for example, storing text embeddings and querying for nearest neighbors, filtering by distance threshold, or combining vector search with record-level access control. See [adding-tables-with-schemas.md](adding-tables-with-schemas.md) for how to define the surrounding table schema.
|
|
@@ -549,6 +648,8 @@ let results = Document.search(
|
|
|
549
648
|
|
|
550
649
|
Instructions for the agent to follow when storing and retrieving large binary content using the `Blob` data type in Harper.
|
|
551
650
|
|
|
651
|
+
**Instead of:** S3 or GCS buckets for app-managed files. Use this rule rather than adding that service. See `harper-architecture`.
|
|
652
|
+
|
|
552
653
|
#### When to Use
|
|
553
654
|
|
|
554
655
|
Apply this rule when a schema field needs to store large binary content such as images, video, audio, or large HTML — typically content larger than 20KB. Use `Blob` instead of `Bytes` when streaming support and out-of-record storage are required. See [handling-binary-data](handling-binary-data.md) for broader binary data guidance.
|
|
@@ -651,6 +752,8 @@ await MyTable.put({ id: 'my-record', data: blob });
|
|
|
651
752
|
|
|
652
753
|
Instructions for the agent to follow when storing and serving binary data (images, audio, arbitrary content types) in Harper.
|
|
653
754
|
|
|
755
|
+
**Instead of:** S3 or GCS buckets for app-managed files. Use this rule rather than adding that service. See `harper-architecture`.
|
|
756
|
+
|
|
654
757
|
#### When to Use
|
|
655
758
|
|
|
656
759
|
Apply this rule when a Harper resource needs to accept, store, or serve binary payloads such as images, audio files, or calendar data. Use it when clients send raw binary via `PUT`/`POST`, or when they send `base64`-encoded data inside a JSON body.
|
|
@@ -767,6 +870,8 @@ export class Photo extends tables.Photo {
|
|
|
767
870
|
|
|
768
871
|
Instructions for the agent to follow when using Harper's automatically generated REST and WebSocket APIs for exported tables and resources.
|
|
769
872
|
|
|
873
|
+
**Instead of:** Hand-written CRUD routes (Express, Fastify, Next.js API routes). Use this rule rather than adding that service. See `harper-architecture`.
|
|
874
|
+
|
|
770
875
|
#### When to Use
|
|
771
876
|
|
|
772
877
|
Apply this rule when enabling HTTP REST endpoints or WebSocket subscriptions for Harper tables and custom resources. Use it whenever you need to configure the REST plugin, understand the auto-generated endpoint surface, or wire up real-time WebSocket connections. See [querying-rest-apis.md](querying-rest-apis.md) for query syntax and [real-time-apps.md](real-time-apps.md) for real-time patterns.
|
|
@@ -958,30 +1063,25 @@ export class InternalLog extends Resource {
|
|
|
958
1063
|
- Server-Sent Events subscriptions are served on the same paths, negotiated via `Accept: text/event-stream`; they are not affected by the `webSocket` option.
|
|
959
1064
|
- See [querying-rest-apis.md](querying-rest-apis.md) for full query syntax on collection endpoints and [real-time-apps.md](real-time-apps.md) for real-time subscription patterns.
|
|
960
1065
|
|
|
961
|
-
### 2.2 Querying REST APIs
|
|
1066
|
+
### 2.2 Querying Harper REST APIs
|
|
962
1067
|
|
|
963
|
-
Instructions for the agent to filter, sort, select, and paginate records
|
|
1068
|
+
Instructions for the agent to filter, sort, select, and paginate records using Harper's URL-based query language on REST collection endpoints.
|
|
964
1069
|
|
|
965
1070
|
#### When to Use
|
|
966
1071
|
|
|
967
|
-
Apply this rule whenever
|
|
1072
|
+
Apply this rule whenever building or modifying REST requests against Harper collection paths that need attribute filtering, comparison operators, sorting, field selection, pagination, or relationship joins. This rule covers all query parameter construction for Harper's automatic REST APIs (see [automatic-apis.md](automatic-apis.md)).
|
|
968
1073
|
|
|
969
1074
|
#### How It Works
|
|
970
1075
|
|
|
971
|
-
1. **Filter by attribute**: Add query parameters matching attribute
|
|
1076
|
+
1. **Filter by attribute**: Add query parameters matching attribute name to value. The queried attribute must be indexed.
|
|
972
1077
|
|
|
973
1078
|
```
|
|
974
1079
|
GET /Product/?category=software
|
|
975
1080
|
GET /Product/?category=software&inStock=true
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
To filter for null values:
|
|
979
|
-
|
|
980
|
-
```
|
|
981
1081
|
GET /Product/?discount=null
|
|
982
1082
|
```
|
|
983
1083
|
|
|
984
|
-
2. **Apply comparison operators (FIQL syntax)**: Use FIQL operators in the query string for range and
|
|
1084
|
+
2. **Apply comparison operators (FIQL syntax)**: Use FIQL operators in the query string for range and string comparisons.
|
|
985
1085
|
|
|
986
1086
|
| Operator | Meaning |
|
|
987
1087
|
| -------------------- | -------------------------------------- |
|
|
@@ -1002,21 +1102,18 @@ Apply this rule whenever you need to construct or handle GET requests against Ha
|
|
|
1002
1102
|
GET /Product/?price=le=20
|
|
1003
1103
|
GET /Product/?name==Keyboard*
|
|
1004
1104
|
GET /Product/?category=software&price=gt=100&price=lt=200
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
For date fields, URL-encode colons as `%3A`:
|
|
1008
|
-
|
|
1009
|
-
```
|
|
1010
1105
|
GET /Product/?listDate=gt=2017-03-08T09%3A30%3A00.000Z
|
|
1011
1106
|
```
|
|
1012
1107
|
|
|
1013
|
-
3. **Chain conditions for range queries**: Omit the attribute name on the second condition to apply it to the same attribute.
|
|
1108
|
+
3. **Chain conditions for range queries**: Omit the attribute name on the second condition to apply it to the same attribute.
|
|
1014
1109
|
|
|
1015
1110
|
```
|
|
1016
1111
|
GET /Product/?price=gt=100<=200
|
|
1017
1112
|
```
|
|
1018
1113
|
|
|
1019
|
-
|
|
1114
|
+
Chaining supports `gt`/`ge` combined with `lt`/`le` only.
|
|
1115
|
+
|
|
1116
|
+
4. **Apply type conversion**: For FIQL comparators, Harper converts values automatically. Use explicit type prefixes when needed.
|
|
1020
1117
|
|
|
1021
1118
|
| Syntax | Behavior |
|
|
1022
1119
|
| ----------------------------------------- | ------------------------------------------- |
|
|
@@ -1036,80 +1133,83 @@ Apply this rule whenever you need to construct or handle GET requests against Ha
|
|
|
1036
1133
|
GET /Product/?rating=5|featured=true
|
|
1037
1134
|
```
|
|
1038
1135
|
|
|
1039
|
-
6. **Group conditions**: Use parentheses or square brackets to control
|
|
1136
|
+
6. **Group conditions**: Use parentheses or square brackets to control order of operations. Prefer square brackets when constructing queries from user input.
|
|
1040
1137
|
|
|
1041
1138
|
```
|
|
1042
1139
|
GET /Product/?rating=5|(price=gt=100&price=lt=200)
|
|
1043
1140
|
GET /Product/?rating=5&[tag=fast|tag=scalable|tag=efficient]
|
|
1044
1141
|
```
|
|
1045
1142
|
|
|
1046
|
-
|
|
1143
|
+
Construct from JavaScript:
|
|
1047
1144
|
|
|
1048
1145
|
```javascript
|
|
1049
1146
|
let url = `/Product/?rating=5&[${tags.map(encodeURIComponent).join('|')}]`;
|
|
1050
1147
|
```
|
|
1051
1148
|
|
|
1052
|
-
7. **Select specific properties with `select()`**:
|
|
1149
|
+
7. **Select specific properties with `select()`**: Specify which properties to include in the response.
|
|
1053
1150
|
|
|
1054
|
-
| Syntax | Returns
|
|
1055
|
-
| -------------------------------------- |
|
|
1056
|
-
| `?select(property)` | Values of a single property directly
|
|
1057
|
-
| `?select(property1,property2)` | Objects with only the specified properties
|
|
1058
|
-
| `?select([property1,property2])` | Arrays of property values
|
|
1059
|
-
| `?select(property1,)` | Objects with a single specified property
|
|
1060
|
-
| `?select(property{subProp1,subProp2})` | Nested objects with specific sub-properties
|
|
1151
|
+
| Syntax | Returns |
|
|
1152
|
+
| -------------------------------------- | ------------------------------------------------- |
|
|
1153
|
+
| `?select(property)` | Values of a single property directly |
|
|
1154
|
+
| `?select(property1,property2)` | Objects with only the specified properties |
|
|
1155
|
+
| `?select([property1,property2])` | Arrays of property values |
|
|
1156
|
+
| `?select(property1,)` | Objects with a single specified property |
|
|
1157
|
+
| `?select(property{subProp1,subProp2})` | Nested objects with specific sub-properties |
|
|
1158
|
+
| `?select(id,products[limit(1)])` | Parents with a constrained relationship selection |
|
|
1061
1159
|
|
|
1062
1160
|
```
|
|
1063
1161
|
GET /Product/?category=software&select(name)
|
|
1064
1162
|
GET /Product/?brand.name=Microsoft&select(name,brand{name})
|
|
1065
1163
|
```
|
|
1066
1164
|
|
|
1067
|
-
8. **Limit results with `limit(end)` or `limit(start,end)`**:
|
|
1165
|
+
8. **Limit results with `limit(end)` or `limit(start,end)`**: Return records in a zero-based window from `start` (inclusive) to `end` (exclusive). `limit(end)` starts at zero.
|
|
1068
1166
|
|
|
1069
1167
|
```
|
|
1070
1168
|
GET /Product/?rating=gt=3&inStock=true&select(rating,name)&limit(20)
|
|
1071
1169
|
GET /Product/?rating=gt=3&limit(10,30)
|
|
1072
1170
|
```
|
|
1073
1171
|
|
|
1074
|
-
9. **Sort results with `sort(property)` or `sort(+property,-property,...)`**: Prefix `+` or no prefix = ascending; `-` = descending.
|
|
1172
|
+
9. **Sort results with `sort(property)` or `sort(+property,-property,...)`**: Prefix `+` or no prefix = ascending; `-` = descending. Multiple properties break ties in order.
|
|
1075
1173
|
|
|
1076
1174
|
```
|
|
1077
1175
|
GET /Product/?rating=gt=3&sort(+name)
|
|
1078
1176
|
GET /Product/?sort(+rating,-price)
|
|
1079
1177
|
```
|
|
1080
1178
|
|
|
1081
|
-
10. **
|
|
1179
|
+
10. **Request a total count for pagination**: Send a `Prefer` header on `GET` or `HEAD` requests. Counting requires a bounded `limit()` with page size no larger than 10,000 and window end no larger than 1,000,000.
|
|
1082
1180
|
|
|
1083
|
-
| `Prefer` value | Meaning
|
|
1084
|
-
| ----------------- |
|
|
1085
|
-
| `count=exact` | Exact
|
|
1086
|
-
| `count=estimated` | Fast approximate count
|
|
1181
|
+
| `Prefer` value | Meaning |
|
|
1182
|
+
| ----------------- | --------------------------------------------------------------------------------------------- |
|
|
1183
|
+
| `count=exact` | Exact number of matching records. Off by default; requires `exactCount: true` in REST config. |
|
|
1184
|
+
| `count=estimated` | Fast approximate count. Does not require `exactCount`. |
|
|
1087
1185
|
|
|
1088
1186
|
```
|
|
1089
1187
|
GET /Product/?category=software&limit(0,25)
|
|
1090
1188
|
Prefer: count=exact
|
|
1091
1189
|
```
|
|
1092
1190
|
|
|
1093
|
-
|
|
1191
|
+
Response headers:
|
|
1094
1192
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
The response status is always `200`. When the total cannot be produced, it is reported as `*` (e.g., `Content-Range: items 0-24/*`).
|
|
1193
|
+
```
|
|
1194
|
+
HTTP/1.1 200 OK
|
|
1195
|
+
Content-Range: items 0-24/1234
|
|
1196
|
+
Range-Unit: items
|
|
1197
|
+
Preference-Applied: count=exact
|
|
1198
|
+
```
|
|
1102
1199
|
|
|
1103
|
-
Enable exact counts in
|
|
1200
|
+
Enable exact counts in REST configuration:
|
|
1104
1201
|
|
|
1105
1202
|
```yaml
|
|
1106
1203
|
rest:
|
|
1107
1204
|
exactCount: true
|
|
1108
1205
|
```
|
|
1109
1206
|
|
|
1110
|
-
|
|
1207
|
+
- The response status for paginated requests is always `200`; Harper does not use `206 Partial Content`.
|
|
1208
|
+
- A `HEAD` request with a `count` preference returns the count headers with no body.
|
|
1209
|
+
- When CORS is enabled, `Content-Range`, `Range-Unit`, and `Preference-Applied` are added to `Access-Control-Expose-Headers`.
|
|
1210
|
+
- Without `exactCount: true`, a `count=exact` request is served as an estimate. When the total cannot be produced, it is reported as `*` (e.g., `Content-Range: items 0-24/*`). `Preference-Applied` still echoes the requested mode, so an unavailable total (`.../*`) is distinct from a request that asked for no count.
|
|
1111
1211
|
|
|
1112
|
-
11. **Query across relationships
|
|
1212
|
+
11. **Query across relationships**: Use dot-syntax on attributes defined with `@relationship` in the schema.
|
|
1113
1213
|
|
|
1114
1214
|
```
|
|
1115
1215
|
GET /Product/?brand.name=Microsoft
|
|
@@ -1117,32 +1217,47 @@ Apply this rule whenever you need to construct or handle GET requests against Ha
|
|
|
1117
1217
|
GET /Product/?brand.name=Microsoft&select(name,brand{name})
|
|
1118
1218
|
```
|
|
1119
1219
|
|
|
1120
|
-
|
|
1220
|
+
Selecting a relationship without an outer dot-syntax filter behaves as a LEFT JOIN. An outer filter such as `products.price=ge=number:30` removes parents without a qualifying product and also narrows the selected products.
|
|
1121
1221
|
|
|
1122
|
-
12. **
|
|
1222
|
+
12. **Filter and page nested relationships**: Use square brackets inside `select()` to apply conditions, sort, and pagination to each parent's related records.
|
|
1123
1223
|
|
|
1124
1224
|
```
|
|
1125
|
-
GET /
|
|
1225
|
+
GET /Brand/?select(id,name,products[price=ge=number:30&sort(price)&limit(1)&select(id,name,price)])
|
|
1126
1226
|
```
|
|
1127
1227
|
|
|
1128
|
-
|
|
1228
|
+
An outer relationship filter also narrows that relationship's nested results — do not repeat the outer filter inside `[...]` unless you want to narrow further.
|
|
1229
|
+
|
|
1230
|
+
| Nested option | Behavior |
|
|
1231
|
+
| ------------------ | --------------------------------------------------------------- |
|
|
1232
|
+
| Conditions | Qualify children; combine with `&` (AND), `\|` (OR), and groups |
|
|
1233
|
+
| `sort(price)` | Order eligible children before pagination |
|
|
1234
|
+
| `limit(end)` | Return at most `end` children per parent |
|
|
1235
|
+
| `limit(start,end)` | Skip `start` eligible children and return at most `end - start` |
|
|
1236
|
+
| `select(id,price)` | Project fields from returned children |
|
|
1237
|
+
- Invalid nested bounds (negative or fractional values) return HTTP 400. `limit(0)` returns no children without reading them.
|
|
1129
1238
|
|
|
1130
1239
|
#### Examples
|
|
1131
1240
|
|
|
1132
|
-
|
|
1241
|
+
##### Range filter with sort and limit
|
|
1242
|
+
|
|
1243
|
+
```
|
|
1244
|
+
GET /Product/?category=software&price=gt=100&price=lt=200&sort(+price)&limit(0,25)
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
##### Relationship join with nested selection
|
|
1133
1248
|
|
|
1134
1249
|
```
|
|
1135
|
-
GET /Product/?
|
|
1250
|
+
GET /Product/?brand.name=Microsoft&select(name,brand{name,id})
|
|
1136
1251
|
```
|
|
1137
1252
|
|
|
1138
|
-
|
|
1253
|
+
##### Paginated request with exact count
|
|
1139
1254
|
|
|
1140
1255
|
```
|
|
1141
1256
|
GET /Product/?category=software&limit(0,25)
|
|
1142
1257
|
Prefer: count=exact
|
|
1143
1258
|
```
|
|
1144
1259
|
|
|
1145
|
-
Response
|
|
1260
|
+
Response:
|
|
1146
1261
|
|
|
1147
1262
|
```
|
|
1148
1263
|
HTTP/1.1 200 OK
|
|
@@ -1151,27 +1266,21 @@ Range-Unit: items
|
|
|
1151
1266
|
Preference-Applied: count=exact
|
|
1152
1267
|
```
|
|
1153
1268
|
|
|
1154
|
-
|
|
1269
|
+
##### Nested relationship filter with OR, sort, and pagination
|
|
1155
1270
|
|
|
1156
|
-
```
|
|
1157
|
-
|
|
1158
|
-
id: Long @primaryKey
|
|
1159
|
-
name: String
|
|
1160
|
-
brandId: Long @indexed
|
|
1161
|
-
brand: Brand @relationship(from: "brandId")
|
|
1162
|
-
}
|
|
1163
|
-
type Brand @table @export {
|
|
1164
|
-
id: Long @primaryKey
|
|
1165
|
-
name: String
|
|
1166
|
-
products: [Product] @relationship(to: "brandId")
|
|
1167
|
-
}
|
|
1271
|
+
```
|
|
1272
|
+
GET /Brand/?sort(name,id)&limit(0,20)&select(id,name,products[[price=lt=number:30|price=ge=number:60]&sort(-price,id)&limit(1,2)&select(id,name,price)])
|
|
1168
1273
|
```
|
|
1169
1274
|
|
|
1275
|
+
##### Outer filter inheriting into nested selection
|
|
1276
|
+
|
|
1170
1277
|
```
|
|
1171
|
-
GET /
|
|
1278
|
+
GET /Brand/?products.price=ge=number:30&select(id,name,products[sort(price)&limit(1)&select(id,name,price)])
|
|
1172
1279
|
```
|
|
1173
1280
|
|
|
1174
|
-
|
|
1281
|
+
##### Many-to-many relationship query
|
|
1282
|
+
|
|
1283
|
+
Schema:
|
|
1175
1284
|
|
|
1176
1285
|
```graphql
|
|
1177
1286
|
type Product @table @export {
|
|
@@ -1182,29 +1291,31 @@ type Product @table @export {
|
|
|
1182
1291
|
}
|
|
1183
1292
|
```
|
|
1184
1293
|
|
|
1294
|
+
Query:
|
|
1295
|
+
|
|
1185
1296
|
```
|
|
1186
1297
|
GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
|
|
1187
1298
|
```
|
|
1188
1299
|
|
|
1189
|
-
**OR grouping with user-supplied tags:**
|
|
1190
|
-
|
|
1191
|
-
```javascript
|
|
1192
|
-
let url = `/Product/?rating=5&[${tags.map(encodeURIComponent).join('|')}]`;
|
|
1193
|
-
```
|
|
1194
|
-
|
|
1195
1300
|
#### Notes
|
|
1196
1301
|
|
|
1197
|
-
-
|
|
1198
|
-
-
|
|
1199
|
-
-
|
|
1200
|
-
-
|
|
1201
|
-
-
|
|
1202
|
-
-
|
|
1302
|
+
- Queried attributes must be indexed for filtering to execute; only one attribute in a multi-attribute query needs to be indexed.
|
|
1303
|
+
- URL-encode colons in date values as `%3A` (e.g., `listDate=gt=2017-03-08T09%3A30%3A00.000Z`).
|
|
1304
|
+
- Square brackets are preferred over parentheses when constructing queries programmatically because standard URI encoding safely encodes `[` and `]`.
|
|
1305
|
+
- Brackets inside `select()` after a relationship name (e.g., `select(id,products[limit(1)])`) form a nested relationship query, not a grouping expression.
|
|
1306
|
+
- For full-text fields marked `@fullText`, use operators such as `=matches=`, `=matches_phrase=`, `=matches_prefix=`, `=matches_fuzzy=`, and their `=not_*=` negated forms. Full-text queries cannot provide an exact total count.
|
|
1307
|
+
- `limit(end)` and `limit(start,end)` inside a nested relationship selection apply per parent, not to the total result set.
|
|
1308
|
+
- Always add a unique field as the final sort key (e.g., `sort(name,id)`) to ensure deterministic ordering across pages.
|
|
1309
|
+
- Nested filters require a table-backed relationship; filters on plain JSON projections or full-text predicates inside a relationship selection return HTTP 400.
|
|
1310
|
+
- For streaming JSON-array responses, check for a terminal object shaped like `{ "error": "ErrorName: message" }` and treat the response as failed.
|
|
1311
|
+
- Access a specific property of a record using dot syntax on the record ID: `GET /MyTable/123.propertyName`. This only works for declared schema properties. The suffixes `.json`, `.cbor`, `.msgpack`, and `.csv` are reserved as content-type selectors and take precedence over a property of the same name.
|
|
1203
1312
|
|
|
1204
1313
|
### 2.3 Real-Time Apps with WebSockets and Pub/Sub
|
|
1205
1314
|
|
|
1206
1315
|
Instructions for the agent to follow when building real-time features in Harper using WebSockets and Pub/Sub.
|
|
1207
1316
|
|
|
1317
|
+
**Instead of:** Socket.io, `ws`, Pusher, Ably, Redis pub/sub, a separate MQTT broker. Use this rule rather than adding that service. See `harper-architecture`.
|
|
1318
|
+
|
|
1208
1319
|
#### When to Use
|
|
1209
1320
|
|
|
1210
1321
|
Apply this rule when implementing any feature that requires real-time bidirectional communication, live data streaming, or push-based updates in a Harper application. This includes chat, live dashboards, sensor feeds, and any scenario where clients must receive resource changes as they happen.
|
|
@@ -1297,6 +1408,8 @@ export class Example extends Resource {
|
|
|
1297
1408
|
|
|
1298
1409
|
Instructions for the agent to handle user authentication, sessions, and JWT token issuance in Harper Resources.
|
|
1299
1410
|
|
|
1411
|
+
**Instead of:** Hand-rolled sessions, `passport`, `jsonwebtoken`, `bcrypt`. Use this rule rather than adding that service. See `harper-architecture`.
|
|
1412
|
+
|
|
1300
1413
|
#### When to Use
|
|
1301
1414
|
|
|
1302
1415
|
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.
|
|
@@ -1603,6 +1716,8 @@ curl --location --request POST 'http://localhost:9925' \
|
|
|
1603
1716
|
|
|
1604
1717
|
Instructions for the agent to follow when defining custom REST endpoints with JavaScript or TypeScript in Harper.
|
|
1605
1718
|
|
|
1719
|
+
**Instead of:** Express, Fastify, or Koa route handlers; serverless functions. Use this rule rather than adding that service. See `harper-architecture`.
|
|
1720
|
+
|
|
1606
1721
|
#### When to Use
|
|
1607
1722
|
|
|
1608
1723
|
Apply this rule when you need to create custom HTTP endpoints, wrap external APIs, or register routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed schema, or when you need fine-grained control over URL shape and path parameters.
|
|
@@ -1773,6 +1888,8 @@ server.resources.set('my-path', Foo);
|
|
|
1773
1888
|
|
|
1774
1889
|
Instructions for the agent to follow when adding custom logic to automatically generated table resources in Harper.
|
|
1775
1890
|
|
|
1891
|
+
**Instead of:** Middleware and controllers wrapped around CRUD routes. Use this rule rather than adding that service. See `harper-architecture`.
|
|
1892
|
+
|
|
1776
1893
|
#### When to Use
|
|
1777
1894
|
|
|
1778
1895
|
Apply this rule when you need to override or augment the default HTTP handler behavior (GET, POST, PUT, PATCH, DELETE) for a Harper table resource. Use it when a table needs computed properties, input transformation, authorization checks, or custom error responses.
|
|
@@ -1867,149 +1984,155 @@ export class MyTable extends tables.MyTable {
|
|
|
1867
1984
|
|
|
1868
1985
|
### 3.3 Programmatic Table Requests
|
|
1869
1986
|
|
|
1870
|
-
Instructions for the agent to
|
|
1987
|
+
Instructions for the agent to interact with Harper tables programmatically using the `tables` object, the Query API, and transactions.
|
|
1988
|
+
|
|
1989
|
+
**Instead of:** ORM and query-builder clients (Prisma, Drizzle, Knex). Use this rule rather than adding that service. See `harper-architecture`.
|
|
1871
1990
|
|
|
1872
1991
|
#### When to Use
|
|
1873
1992
|
|
|
1874
|
-
Apply this rule when writing server-side Harper
|
|
1993
|
+
Apply this rule when writing server-side Harper code that reads, writes, searches, or transactionally modifies table data through the `tables` object. Use it when building background jobs, SSR renderers, or any code that calls `search(`, `addTo`, `select`, `sort`, or `transaction()` directly rather than through REST endpoints.
|
|
1875
1994
|
|
|
1876
1995
|
#### How It Works
|
|
1877
1996
|
|
|
1878
|
-
1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a property of `tables`.
|
|
1997
|
+
1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a named property of `tables`. This is equivalent to `databases.data.<TableName>`.
|
|
1879
1998
|
|
|
1880
1999
|
```javascript
|
|
1881
|
-
import { tables,
|
|
2000
|
+
import { tables, Resource } from 'harper';
|
|
1882
2001
|
const { Product } = tables;
|
|
1883
2002
|
// same as: databases.data.Product
|
|
1884
2003
|
```
|
|
1885
2004
|
|
|
1886
|
-
|
|
2005
|
+
CommonJS is also supported: `const { tables, Resource } = require('harper');`
|
|
2006
|
+
|
|
2007
|
+
For components in their own directory, link the package first:
|
|
1887
2008
|
|
|
1888
2009
|
```bash
|
|
1889
2010
|
npm link harper
|
|
1890
2011
|
```
|
|
1891
2012
|
|
|
1892
|
-
|
|
2013
|
+
When bundling for SSR with Vite, keep `harper` external so it resolves to the runtime:
|
|
1893
2014
|
|
|
1894
|
-
|
|
2015
|
+
```javascript
|
|
2016
|
+
// vite.config
|
|
2017
|
+
ssr: {
|
|
2018
|
+
external: ['harper'];
|
|
2019
|
+
}
|
|
2020
|
+
```
|
|
2021
|
+
|
|
2022
|
+
2. **Define your schema with `@table` (and `@indexed` where needed)**: Tables must be declared in `schema.graphql`. Mark attributes that will be used as sort keys or standalone condition attributes with `@indexed`.
|
|
1895
2023
|
|
|
1896
2024
|
```graphql
|
|
1897
2025
|
type Product @table {
|
|
1898
2026
|
id: Long @primaryKey
|
|
1899
2027
|
name: String
|
|
1900
|
-
price: Float
|
|
2028
|
+
price: Float @indexed
|
|
1901
2029
|
}
|
|
1902
2030
|
```
|
|
1903
2031
|
|
|
1904
|
-
3. **Query records using `search(`**: Pass a Query object to `search()`. The query is an async iterable.
|
|
2032
|
+
3. **Query records using `search(`**: Pass a Query object to `search()`. The query is an async iterable; iterate the result with `for await`. Use a `conditions` array to filter records. Each condition specifies `attribute`, `value`, and an optional `comparator`.
|
|
2033
|
+
|
|
2034
|
+
| Property | Description |
|
|
2035
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2036
|
+
| `attribute` | Property name, or array for chained/joined properties |
|
|
2037
|
+
| `value` | The value to match |
|
|
2038
|
+
| `comparator` | `equals` (default), `greater_than`, `greater_than_equal`, `less_than`, `less_than_equal`, `starts_with`, `contains`, `ends_with`, `between`, `not_equal`, or a full-text comparator |
|
|
2039
|
+
| `conditions` | Nested conditions array |
|
|
2040
|
+
| `operator` | `and` (default) or `or` for the nested `conditions` |
|
|
1905
2041
|
|
|
1906
2042
|
```javascript
|
|
1907
|
-
const
|
|
2043
|
+
for await (const record of Product.search({
|
|
1908
2044
|
conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
|
|
1909
|
-
}
|
|
1910
|
-
for await (const record of Product.search(query)) {
|
|
2045
|
+
})) {
|
|
1911
2046
|
// process record
|
|
1912
2047
|
}
|
|
1913
2048
|
```
|
|
1914
2049
|
|
|
1915
|
-
4. **
|
|
2050
|
+
4. **Use `select` to control which properties are returned**: Pass an array of property names, a string for a single property, or nested objects for relationships.
|
|
2051
|
+
|
|
2052
|
+
```javascript
|
|
2053
|
+
const book = await Book.get({
|
|
2054
|
+
id: 42,
|
|
2055
|
+
select: ['id', 'title', { name: 'author', select: ['name'] }],
|
|
2056
|
+
});
|
|
2057
|
+
```
|
|
1916
2058
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
| `operator` | `and` (default) or `or` for the nested `conditions` |
|
|
2059
|
+
Special `select` properties:
|
|
2060
|
+
- `$id` — returns the primary key regardless of its name
|
|
2061
|
+
- `$updatedtime` — returns the last-updated timestamp
|
|
2062
|
+
- `$distance` — returns computed vector distance when using a vector index
|
|
2063
|
+
- `$score` — returns BM25 relevance score for full-text queries
|
|
2064
|
+
- `$highlights` — returns full-text fragments and matching spans
|
|
1924
2065
|
|
|
1925
|
-
5. **
|
|
2066
|
+
5. **Use `sort` with an `@indexed` attribute or combine with `conditions`**: Harper requires either an `@indexed` sort attribute or at least one `conditions` entry when sorting. Sorting by a non-indexed attribute with zero conditions raises:
|
|
1926
2067
|
|
|
1927
|
-
|
|
1928
|
-
// Flat select
|
|
1929
|
-
Product.search({ select: ['name', 'price'] });
|
|
2068
|
+
> `HdbError: <attribute> is not indexed and not combined with any other conditions`
|
|
1930
2069
|
|
|
1931
|
-
|
|
1932
|
-
Book.get({ id: 42, select: ['id', 'title', { name: 'author', select: ['name'] }] });
|
|
1933
|
-
```
|
|
2070
|
+
Note: `@primaryKey` alone is treated as not indexed for sort purposes. To iterate a whole table in primary-key order, add an open-ended condition:
|
|
1934
2071
|
|
|
1935
|
-
|
|
2072
|
+
```javascript
|
|
2073
|
+
Product.search({
|
|
2074
|
+
conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
|
|
2075
|
+
sort: { attribute: 'id' },
|
|
2076
|
+
});
|
|
2077
|
+
```
|
|
1936
2078
|
|
|
1937
|
-
|
|
2079
|
+
`sort` object properties:
|
|
1938
2080
|
|
|
1939
2081
|
| Property | Description |
|
|
1940
2082
|
| ------------ | ---------------------------------------------------------- |
|
|
1941
2083
|
| `attribute` | Property name (or array for chained relationship property) |
|
|
1942
2084
|
| `descending` | Sort descending if `true` (default: `false`) |
|
|
1943
2085
|
| `next` | Secondary sort to resolve ties (same structure) |
|
|
1944
|
-
- If the sort `attribute` is `@indexed`, no condition is required.
|
|
1945
|
-
- If the sort `attribute` is not indexed, at least one entry in `conditions` (on **any** attribute) is required.
|
|
1946
|
-
- Sorting by a non-indexed attribute with zero conditions raises:
|
|
1947
|
-
> `HdbError: <attribute> is not indexed and not combined with any other conditions`
|
|
1948
|
-
- The bare `@primaryKey` is treated as not indexed for sort purposes. To iterate in primary-key order, add an open-ended condition:
|
|
1949
|
-
|
|
1950
|
-
```javascript
|
|
1951
|
-
Product.search({
|
|
1952
|
-
conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
|
|
1953
|
-
sort: { attribute: 'id' },
|
|
1954
|
-
});
|
|
1955
|
-
```
|
|
1956
|
-
|
|
1957
|
-
- Pass `allowFullScan: true` to permit an unconditional ordered scan, or omit `sort` entirely to iterate without an index requirement.
|
|
1958
2086
|
|
|
1959
|
-
|
|
2087
|
+
6. **Apply `limit` and `offset` for pagination**: Add these to the Query object to bound result sets.
|
|
1960
2088
|
|
|
1961
2089
|
```javascript
|
|
1962
|
-
Product.search({
|
|
2090
|
+
Product.search({
|
|
2091
|
+
conditions: [{ attribute: 'price', comparator: 'less_than', value: 100 }],
|
|
2092
|
+
limit: 20,
|
|
2093
|
+
offset: 40,
|
|
2094
|
+
});
|
|
1963
2095
|
```
|
|
1964
2096
|
|
|
1965
|
-
|
|
2097
|
+
7. **Use `explain` and `enforceExecutionOrder` for debugging and optimization**:
|
|
1966
2098
|
- `explain: true` — returns conditions reordered as Harper will execute them.
|
|
1967
2099
|
- `enforceExecutionOrder: true` — forces conditions to execute in the order supplied, disabling Harper's automatic re-ordering.
|
|
1968
2100
|
|
|
1969
|
-
|
|
2101
|
+
8. **Use `addTo` for concurrent-safe numeric updates**: Increments or decrements a property using CRDT incrementation, safe across threads and nodes.
|
|
1970
2102
|
|
|
1971
2103
|
```javascript
|
|
1972
2104
|
static async post(target, data) {
|
|
1973
2105
|
const record = await this.update(target.id);
|
|
1974
|
-
record.addTo('quantity', -1);
|
|
2106
|
+
record.addTo('quantity', -1); // decrement safely across nodes
|
|
1975
2107
|
}
|
|
1976
2108
|
```
|
|
1977
2109
|
|
|
1978
|
-
|
|
2110
|
+
9. **Wrap background or timer work in `transaction()`**: HTTP handlers get a transaction automatically. Use `transaction()` explicitly for timers, background jobs, or any code outside a natural request context. Always `await` the call and `catch` errors.
|
|
1979
2111
|
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
The `txn` object exposes:
|
|
1989
|
-
|
|
1990
|
-
| Member | Type | Description |
|
|
1991
|
-
| --------------------- | --------------- | ------------------------------------------------------ |
|
|
1992
|
-
| `commit()` | `() => Promise` | Commits the current transaction |
|
|
1993
|
-
| `abort()` | `() => void` | Aborts the transaction and resets it |
|
|
1994
|
-
| `resetReadSnapshot()` | `() => void` | Resets the read snapshot to the latest committed state |
|
|
1995
|
-
| `timestamp` | `number` | Timestamp associated with the current transaction |
|
|
2112
|
+
```javascript
|
|
2113
|
+
await transaction(async (txn) => {
|
|
2114
|
+
for (let item of data) {
|
|
2115
|
+
await MyTable.put(item, txn);
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
```
|
|
1996
2119
|
|
|
1997
|
-
|
|
2120
|
+
The `txn` object exposes:
|
|
1998
2121
|
|
|
1999
|
-
|
|
2122
|
+
| Member | Type | Description |
|
|
2123
|
+
| --------------------- | --------------- | ------------------------------------------------------ |
|
|
2124
|
+
| `commit()` | `() => Promise` | Commits the current transaction |
|
|
2125
|
+
| `abort()` | `() => void` | Aborts the transaction and resets it |
|
|
2126
|
+
| `resetReadSnapshot()` | `() => void` | Resets the read snapshot to the latest committed state |
|
|
2127
|
+
| `timestamp` | `number` | Timestamp associated with the current transaction |
|
|
2000
2128
|
|
|
2001
|
-
|
|
2129
|
+
On normal callback completion the transaction commits automatically. If the callback throws, the transaction is aborted.
|
|
2002
2130
|
|
|
2003
|
-
|
|
2004
|
-
// vite.config
|
|
2005
|
-
ssr: {
|
|
2006
|
-
external: ['harper'];
|
|
2007
|
-
}
|
|
2008
|
-
```
|
|
2131
|
+
10. **Understand atomicity boundaries**: All tables within the same database share a single transactional context — reads return a consistent snapshot and writes are committed atomically. Tables in different databases each get their own transaction with no cross-database atomicity guarantee.
|
|
2009
2132
|
|
|
2010
2133
|
#### Examples
|
|
2011
2134
|
|
|
2012
|
-
|
|
2135
|
+
##### Basic CRUD and search
|
|
2013
2136
|
|
|
2014
2137
|
```javascript
|
|
2015
2138
|
import { tables } from 'harper';
|
|
@@ -2025,14 +2148,15 @@ await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) /
|
|
|
2025
2148
|
const record = await Product.get(created.id);
|
|
2026
2149
|
|
|
2027
2150
|
// Search with conditions
|
|
2028
|
-
|
|
2151
|
+
const query = {
|
|
2029
2152
|
conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
|
|
2030
|
-
}
|
|
2153
|
+
};
|
|
2154
|
+
for await (const record of Product.search(query)) {
|
|
2031
2155
|
// process record
|
|
2032
2156
|
}
|
|
2033
2157
|
```
|
|
2034
2158
|
|
|
2035
|
-
|
|
2159
|
+
##### Nested conditions with `or`
|
|
2036
2160
|
|
|
2037
2161
|
```javascript
|
|
2038
2162
|
Product.search({
|
|
@@ -2049,13 +2173,32 @@ Product.search({
|
|
|
2049
2173
|
});
|
|
2050
2174
|
```
|
|
2051
2175
|
|
|
2052
|
-
|
|
2176
|
+
##### Chained attribute reference (join/relationship)
|
|
2053
2177
|
|
|
2054
2178
|
```javascript
|
|
2055
2179
|
Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
|
|
2056
2180
|
```
|
|
2057
2181
|
|
|
2058
|
-
|
|
2182
|
+
##### Selecting and constraining related records
|
|
2183
|
+
|
|
2184
|
+
```javascript
|
|
2185
|
+
const brand = await Brand.get({
|
|
2186
|
+
id: 42,
|
|
2187
|
+
select: [
|
|
2188
|
+
'id',
|
|
2189
|
+
{
|
|
2190
|
+
name: 'products',
|
|
2191
|
+
conditions: [{ attribute: 'price', comparator: 'greater_than_equal', value: 30 }],
|
|
2192
|
+
sort: { attribute: 'price' },
|
|
2193
|
+
offset: 0,
|
|
2194
|
+
limit: 1,
|
|
2195
|
+
select: ['id', 'price'],
|
|
2196
|
+
},
|
|
2197
|
+
],
|
|
2198
|
+
});
|
|
2199
|
+
```
|
|
2200
|
+
|
|
2201
|
+
##### Background job with `transaction()`
|
|
2059
2202
|
|
|
2060
2203
|
```javascript
|
|
2061
2204
|
import { isMainThread } from 'node:worker_threads';
|
|
@@ -2065,7 +2208,7 @@ const { MyTable } = tables;
|
|
|
2065
2208
|
if (isMainThread) {
|
|
2066
2209
|
let running = false;
|
|
2067
2210
|
setInterval(async () => {
|
|
2068
|
-
if (running) return;
|
|
2211
|
+
if (running) return; // the previous run has not committed yet
|
|
2069
2212
|
running = true;
|
|
2070
2213
|
try {
|
|
2071
2214
|
let data = await (await fetch('https://example.com/data')).json();
|
|
@@ -2079,11 +2222,11 @@ if (isMainThread) {
|
|
|
2079
2222
|
} finally {
|
|
2080
2223
|
running = false;
|
|
2081
2224
|
}
|
|
2082
|
-
}, 3600000);
|
|
2225
|
+
}, 3600000); // every hour
|
|
2083
2226
|
}
|
|
2084
2227
|
```
|
|
2085
2228
|
|
|
2086
|
-
|
|
2229
|
+
##### SSR with Vite
|
|
2087
2230
|
|
|
2088
2231
|
```typescript
|
|
2089
2232
|
import { tables } from 'harper';
|
|
@@ -2094,19 +2237,28 @@ export async function render(url: string): Promise<string> {
|
|
|
2094
2237
|
}
|
|
2095
2238
|
```
|
|
2096
2239
|
|
|
2240
|
+
Keep `harper` external in `vite.config` so it resolves to the runtime:
|
|
2241
|
+
|
|
2242
|
+
```javascript
|
|
2243
|
+
ssr: {
|
|
2244
|
+
external: ['harper'];
|
|
2245
|
+
}
|
|
2246
|
+
```
|
|
2247
|
+
|
|
2097
2248
|
#### Notes
|
|
2098
2249
|
|
|
2099
|
-
-
|
|
2100
|
-
-
|
|
2101
|
-
-
|
|
2102
|
-
-
|
|
2103
|
-
- Guard against
|
|
2104
|
-
- CommonJS is also supported: `const { tables, Resource } = require('harper');`
|
|
2250
|
+
- Calls through `tables` run in a trusted server-side context and do **not** automatically apply the target table's role permissions. Set `checkPermission: true` on the query and pass the authenticated `context` as the second argument when authorization is required.
|
|
2251
|
+
- `update`, `patch`, and `delete` operate directly on stored data. Scope destructive operations with specific `conditions`, validate the affected set before writing, and gate them behind authorization controls.
|
|
2252
|
+
- Pass `allowFullScan: true` to permit an unconditional ordered scan without conditions. If order doesn't matter, omit `sort` entirely — `search({})` iterates without an index requirement.
|
|
2253
|
+
- If `transaction()` is called with a context that already has an active transaction, it reuses that transaction and executes the callback immediately — safe to call defensively.
|
|
2254
|
+
- Guard timer-based jobs against overlap: a timer fires on schedule regardless of whether the previous run has committed. Use a `running` flag to skip a tick when the previous run is still active.
|
|
2105
2255
|
|
|
2106
2256
|
### 3.4 TypeScript Type Stripping in Harper
|
|
2107
2257
|
|
|
2108
2258
|
Instructions for the agent to run `.ts` files directly in Harper without a build step using Node.js's built-in type stripping.
|
|
2109
2259
|
|
|
2260
|
+
**Instead of:** `tsc` or `ts-node` build steps for server code. Use this rule rather than adding that service. See `harper-architecture`.
|
|
2261
|
+
|
|
2110
2262
|
#### When to Use
|
|
2111
2263
|
|
|
2112
2264
|
Apply this rule when writing Harper resource files in TypeScript. Use it any time you need to reference `.ts` source files from `config.yaml` or import between local TypeScript modules in a Harper project.
|
|
@@ -2161,6 +2313,8 @@ jsResource:
|
|
|
2161
2313
|
|
|
2162
2314
|
Instructions for the agent to implement integrated data caching from external sources using Harper's cache table directives and `sourcedFrom` API.
|
|
2163
2315
|
|
|
2316
|
+
**Instead of:** Redis or Memcached cache-aside layers. Use this rule rather than adding that service. See `harper-architecture`.
|
|
2317
|
+
|
|
2164
2318
|
#### When to Use
|
|
2165
2319
|
|
|
2166
2320
|
Apply this rule when an application needs to wrap an external API, microservice, or database with a fast local cache. Use it when you need to define TTL-based cache expiration, connect an upstream data source to a Harper table, or implement on-demand cache invalidation.
|
|
@@ -2320,11 +2474,13 @@ await fetch('http://localhost:9926/JokeCache/1', {
|
|
|
2320
2474
|
|
|
2321
2475
|
### 4.1 Deploying to Harper Fabric
|
|
2322
2476
|
|
|
2323
|
-
Instructions for the agent to
|
|
2477
|
+
Instructions for the agent to deploy a Harper application to Harper Fabric using the CLI, verify certification, and roll back using a previous release's `deployment_id`.
|
|
2478
|
+
|
|
2479
|
+
**Instead of:** Docker Compose stacks; deploying several services separately. Use this rule rather than adding that service. See `harper-architecture`.
|
|
2324
2480
|
|
|
2325
2481
|
#### When to Use
|
|
2326
2482
|
|
|
2327
|
-
Apply this rule when deploying to a remote cluster,
|
|
2483
|
+
Apply this rule when deploying a Harper application to a remote Harper Fabric cluster, checking whether a rolling deploy was certified on each node, or restoring a previous release without rebuilding. Also apply it when provisioning credentials for private-source deploys or staging a build for later activation.
|
|
2328
2484
|
|
|
2329
2485
|
#### How It Works
|
|
2330
2486
|
|
|
@@ -2335,10 +2491,9 @@ Apply this rule when deploying to a remote cluster, choosing how it restarts, re
|
|
|
2335
2491
|
# Provide cluster username and password when prompted
|
|
2336
2492
|
```
|
|
2337
2493
|
|
|
2338
|
-
|
|
2339
|
-
- For a one-off command, `auth_username=` and `auth_password=` also work, and take precedence over environment variables and saved login tokens. Don't use them in production.
|
|
2494
|
+
For CI/CD pipelines, use workload identity (OIDC) or a refresh token from `harper login --for-ci` instead of a password. See [deploying-from-ci.md](deploying-from-ci.md) for a complete GitHub Actions workflow. For a one-off command, `auth_username=` and `auth_password=` also work, and take precedence over environment variables and saved login tokens. Don't use them in production.
|
|
2340
2495
|
|
|
2341
|
-
2. **Deploy
|
|
2496
|
+
2. **Deploy the application**: Run `harper deploy` with the required parameters. Omit `package` to upload the current directory; supply a reference for npm, GitHub, or tarball sources.
|
|
2342
2497
|
|
|
2343
2498
|
```bash
|
|
2344
2499
|
harper deploy \
|
|
@@ -2349,54 +2504,38 @@ Apply this rule when deploying to a remote cluster, choosing how it restarts, re
|
|
|
2349
2504
|
replicated=true
|
|
2350
2505
|
```
|
|
2351
2506
|
|
|
2352
|
-
|
|
2353
|
-
- `project` defaults to the current directory's name for a directory deploy, or is derived from the package for a package deploy.
|
|
2354
|
-
- A bare `target` host defaults to `https://<host>:9925`.
|
|
2355
|
-
- Pass `json=true` to print the result as JSON instead of YAML.
|
|
2356
|
-
- Every result carries a `deployment_id`, staged or not. It names this release when you go back to it later (step 4).
|
|
2507
|
+
A bare `target` host defaults to `https://<host>:9925`. Use `restart=rolling` for a staggered, zero-downtime restart across the cluster. Pass `replicated=false` to deploy only to the node you called. Pass `json=true` to print the result as JSON instead of YAML.
|
|
2357
2508
|
|
|
2358
|
-
3. **Choose
|
|
2509
|
+
3. **Choose a package source**: The `package` field accepts any valid npm dependency value.
|
|
2359
2510
|
|
|
2360
|
-
|
|
2511
|
+
| Source | Value |
|
|
2512
|
+
| ------------------ | --------------------------------------------------------------------------------------------- |
|
|
2513
|
+
| npm package | `package="@harperdb/status-check"` |
|
|
2514
|
+
| GitHub (public) | `package="HarperFast/status-check"` or `package="https://github.com/HarperFast/status-check"` |
|
|
2515
|
+
| Private repo (SSH) | `package="git+ssh://git@github.com:HarperDB/secret-app.git"` |
|
|
2516
|
+
| Tarball | `package="https://example.com/application.tar.gz"` |
|
|
2361
2517
|
|
|
2362
|
-
|
|
2363
|
-
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2364
|
-
| `certified` | The canary loaded the release, and the rollout went on. |
|
|
2365
|
-
| `uncertified` | The rollout went on unchecked: the component ran nothing as it loaded (`loadComponent: dev-only`, an absent `if-installed` component, or safe mode), or no worker was replaced. |
|
|
2366
|
-
| `unavailable` | The deploy restarted unchecked: no worker on this node loads the component, its `isolated` setting changed, it was already live, or it was deployed from a local directory (`package: file:<dir>`). |
|
|
2367
|
-
| `not-requested` | The deploy did not restart. |
|
|
2368
|
-
- **A rejected release fails the deploy.** If the canary throws while loading the component, exits, or doesn't report within a minute, the deploy fails with `400`. The release it replaced is made live again, and nothing is replicated to other nodes. The error's `certification` is an object whose `status` is `rejected`, or `interrupted` for a restart that stopped before the canary decided.
|
|
2369
|
-
- **A release with nothing to go back to fails closed.** A rejected first deploy, or one whose previous release was not kept, stays on disk, but no worker on that node loads it, even after Harper restarts. `get_status` reports the component as failed. Deploy a fix, or once the cause is fixed, activate the same `deployment_id` again with `restart`.
|
|
2370
|
-
- **A rolling deploy exits before every node has taken the release.** The other nodes stage it, and the job activates it on each in turn, each with a canary of its own. A node that rejects it keeps the previous release, or fails closed if it had none. Nothing is rolled back on the others, and the job fails naming each node that did not activate it. The deployment record reports `success` while the job runs and after it fails, so poll the job instead: `harper get_job id=<restartJobId>` ends `COMPLETE`, or `ERROR` with a `message` listing each node's outcome under `activated`.
|
|
2371
|
-
- **Poll the node that received the deploy.** A job is recorded only on the node that ran it, so any other node answers `get_job` with an empty array. A role with an `operations` allowlist, or a token with an `operations` scope, must list `get_job` to poll it.
|
|
2372
|
-
- Another deploy or `drop_component` of the component on that node is refused with `409` until the rollout has finished.
|
|
2373
|
-
- A node running a version before v5.4.0 activates the release without a canary.
|
|
2518
|
+
For git tags, use the `semver` directive: `HarperFast/application-template#semver:v1.0.0`. For an SSH-based private repo, register the key with `add_ssh_key` first.
|
|
2374
2519
|
|
|
2375
|
-
4. **
|
|
2520
|
+
4. **Provision credentials for private repositories**: Run `harper deploy setup=true` once per component and source. (With `provider=github-actions`, it sets up GitHub Actions OIDC deploys instead.) This requires `super_user` privileges and is interactive. It calls `get_secrets_public_key`, `set_secret`, and `grant_secret`. Run it with an administrative credential rather than the CI identity it provisions for:
|
|
2376
2521
|
|
|
2377
2522
|
```bash
|
|
2378
|
-
harper
|
|
2379
|
-
harper deploy project=my-app deployment_id=<id> restart=rolling
|
|
2523
|
+
harper deploy setup=true
|
|
2380
2524
|
```
|
|
2381
2525
|
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
"restart": "rolling"
|
|
2388
|
-
}
|
|
2389
|
-
```
|
|
2526
|
+
The command:
|
|
2527
|
+
1. Fetches the cluster's public key with `get_secrets_public_key`.
|
|
2528
|
+
2. Encrypts the token locally into an `enc:v1:` envelope.
|
|
2529
|
+
3. Stores only the ciphertext with `set_secret`, scoped to the component, never in the global `processEnv` tier.
|
|
2530
|
+
4. Grants the component permission to resolve it with `grant_secret`.
|
|
2390
2531
|
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
- A release made live before v5.3.0, or deployed from a `file:` directory, is not kept.
|
|
2397
|
-
- Each node answers for itself. A node that never had the release, or no longer keeps it, answers `404`, which `deploy_component` reports as a failed peer. `409` means the build is there but can't be activated.
|
|
2532
|
+
Use a **fine-grained** personal access token with **Contents: Read-only** on the target repository. Avoid broad tokens: the `gh` CLI session token typically carries `read:org`, `gist`, and `workflow` scopes across your whole account. Then deploy with `credential=true`:
|
|
2533
|
+
|
|
2534
|
+
```bash
|
|
2535
|
+
harper deploy by_ref=true credential=true restart=true replicated=true
|
|
2536
|
+
```
|
|
2398
2537
|
|
|
2399
|
-
5. **Stage a release to make live later**: `activate=false` (`activate: false` in the operation) builds and installs the release on each node without making it live, so the slow, fallible part happens when you choose and the cut-over is quick.
|
|
2538
|
+
5. **Stage a release to make live later**: `activate=false` (`activate: false` in the operation) builds and installs the release on each node without making it live, so the slow, fallible part happens when you choose and the cut-over is quick. Cannot be combined with `restart`.
|
|
2400
2539
|
|
|
2401
2540
|
```json
|
|
2402
2541
|
{
|
|
@@ -2407,6 +2546,8 @@ Apply this rule when deploying to a remote cluster, choosing how it restarts, re
|
|
|
2407
2546
|
}
|
|
2408
2547
|
```
|
|
2409
2548
|
|
|
2549
|
+
The response names the artifact waiting to be activated:
|
|
2550
|
+
|
|
2410
2551
|
```json
|
|
2411
2552
|
{
|
|
2412
2553
|
"deployment_id": "a3f8c2d1-...",
|
|
@@ -2414,147 +2555,214 @@ Apply this rule when deploying to a remote cluster, choosing how it restarts, re
|
|
|
2414
2555
|
}
|
|
2415
2556
|
```
|
|
2416
2557
|
|
|
2417
|
-
Make it live by deploying that `deployment_id`, as in step
|
|
2418
|
-
- Pass `restart` on the activation, not the stage: `restart` is rejected alongside `activate: false`.
|
|
2419
|
-
- The activation takes no `package`, `payload`, `credentials`, install options, or `urlPath`/`host`. They are rejected, because the staged build already decided them.
|
|
2420
|
-
- Activating the release that is already live succeeds without a swap, so if an activation fails partway (a peer was unreachable, or the root config entry wasn't written), retry the same `deployment_id`. A node that already switched answers success, one still holding the build switches now, and one that holds neither answers `404`; the retry also finishes what the failed attempt left half done. With `restart`, a node where the release is already live still restarts every worker, isolated applications' included. Without it, the node marks the component as needing a restart.
|
|
2421
|
-
- A staged build outlives later deploys: stage v2, deploy v3, and activating v2's id returns the component to v2.
|
|
2422
|
-
- `file:` directory sources cannot be staged.
|
|
2423
|
-
- Upgrade every node before staging. A node before v5.3.0 doesn't recognize `activate: false`, and serves the release immediately. A peer that doesn't confirm staging fails the operation with the node names.
|
|
2558
|
+
Nothing about the running component changes, and `get_deployment` reports it as `staged`. Make it live by deploying that `deployment_id`, as in step 6. The activation takes no `package`, `payload`, `credentials`, install options, or `urlPath`/`host`. They are rejected, because the staged build already decided them.
|
|
2424
2559
|
|
|
2425
|
-
6. **
|
|
2560
|
+
6. **Activate a staged build or roll back**: Pass `deployment_id` with `restart` to make a staged build live, or to restore a release a later deploy replaced, with no rebuild or reinstall. Pass `restart` here rather than on the stage.
|
|
2426
2561
|
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
| npm package | `package="@harperdb/status-check"` |
|
|
2431
|
-
| GitHub (public) | `package="HarperFast/status-check"` or `package="https://github.com/HarperFast/status-check"` |
|
|
2432
|
-
| Private repo (SSH) | `package="git+ssh://git@github.com:HarperDB/secret-app.git"` |
|
|
2433
|
-
| Tarball | `package="https://example.com/application.tar.gz"` |
|
|
2562
|
+
```bash
|
|
2563
|
+
harper deploy project=<name> deployment_id=<id> restart=true
|
|
2564
|
+
```
|
|
2434
2565
|
|
|
2435
|
-
|
|
2566
|
+
Use `list_deployments` to find the `deployment_id` of the release to restore:
|
|
2436
2567
|
|
|
2437
|
-
|
|
2568
|
+
```json
|
|
2569
|
+
{ "operation": "list_deployments", "project": "my-app", "status": "success", "limit": 20 }
|
|
2570
|
+
```
|
|
2571
|
+
|
|
2572
|
+
- A `404` response means the id never existed on that node, was pruned, or the release was replaced and not kept. A `409` means the build exists but cannot be activated (e.g., belongs to another component, or files changed after staging).
|
|
2573
|
+
- Kept releases and staged builds count against `deployment.stagingRetention.maxCount` (`stagingRetention` default: `5`). Each kept release is a full installed copy of the component, `node_modules` included, so budget disk for it. `0` keeps no replaced releases.
|
|
2574
|
+
|
|
2575
|
+
7. **Deploy by reference for a reproducible source**: `by_ref=true` sends a pinned git reference instead of uploading the working directory, and the cluster fetches and builds that exact commit. It resolves the repository's `origin` remote and the current commit, then deploys `package=git+https://github.com/<owner>/<repo>.git#<full commit SHA>`. Defaults to `HEAD`.
|
|
2438
2576
|
|
|
2439
2577
|
```bash
|
|
2440
2578
|
harper deploy by_ref=true restart=true replicated=true
|
|
2579
|
+
```
|
|
2580
|
+
|
|
2581
|
+
Deploy a specific tag or commit with `ref`:
|
|
2582
|
+
|
|
2583
|
+
```bash
|
|
2441
2584
|
harper deploy ref=v1.2.0 restart=true replicated=true
|
|
2442
2585
|
harper deploy ref=9f8c2a1 restart=true replicated=true
|
|
2443
2586
|
```
|
|
2444
2587
|
|
|
2445
|
-
-
|
|
2588
|
+
- Tags and branches are resolved to a full commit SHA before the deploy is sent. A `ref` that cannot be resolved stops the deploy — run `git fetch` and retry, or pass a full commit SHA.
|
|
2446
2589
|
- Any other `ref` must name something a clone can fetch: `refs/heads/*` or `refs/tags/*`, or a bare branch or tag name. A qualified ref outside those, such as `refs/pull/123/head`, is rejected up front.
|
|
2447
|
-
- Commit and push first: the cluster clones from the remote. `by_ref` warns when the working tree is dirty, and when the commit isn't on any remote branch. The second check is skipped under GitHub Actions.
|
|
2448
2590
|
- On a GitHub Actions `pull_request` run, `by_ref` deploys the pull request's head commit. If the event payload isn't readable, the deploy stops; pass the commit explicitly.
|
|
2449
|
-
-
|
|
2591
|
+
- Commit and push first — the cluster clones from the remote and only sees pushed commits.
|
|
2592
|
+
|
|
2593
|
+
8. **Check certification after a rolling deploy**: A deploy with `restart=rolling` certifies the release in a canary worker before rolling it out. The deploy response includes a `certification` field:
|
|
2594
|
+
|
|
2595
|
+
| `certification` | Meaning |
|
|
2596
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2597
|
+
| `certified` | The canary loaded the release; rollout proceeded. |
|
|
2598
|
+
| `uncertified` | The rollout went on unchecked: the component ran nothing as it loaded (`loadComponent: dev-only`, an absent `if-installed` component, or safe mode), or no worker was replaced. |
|
|
2599
|
+
| `unavailable` | Deploy restarted unchecked (no worker loads the component, `isolated` setting changed, already live, or `file:` directory source). |
|
|
2600
|
+
| `not-requested` | The deploy did not restart. |
|
|
2601
|
+
- **A rejected release fails the deploy.** If the canary throws while loading the component, exits, or doesn't report within a minute, the deploy fails with `400`. The release it replaced is made live again, and nothing is replicated to other nodes. The error carries the decision as an object in `certification` with `status` set to `rejected`, or to `interrupted` for a restart that stopped before the canary decided.
|
|
2602
|
+
- **A release with nothing to go back to fails closed.** A rejected first deploy, or one whose previous release was not kept, stays on disk, but no worker on that node loads it, even after Harper restarts. `get_status` reports the component as failed and says why. Deploy a fix, or activate the same `deployment_id` again with `restart`.
|
|
2603
|
+
- Another deploy or `drop_component` of the component on that node is refused with `409` until the rollout has finished.
|
|
2450
2604
|
|
|
2451
|
-
|
|
2605
|
+
9. **Wait for a rolling deploy to finish**: A `deploy_component` with `"restart": "rolling"` returns a `restartJobId`. Poll it with `get_job` to learn whether every node took the release.
|
|
2452
2606
|
|
|
2453
|
-
```
|
|
2454
|
-
|
|
2607
|
+
```json
|
|
2608
|
+
{ "operation": "get_job", "id": "4a982782-929a-4507-8794-26dae1132def" }
|
|
2455
2609
|
```
|
|
2456
2610
|
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
2. Encrypts the token locally into an `enc:v1:` envelope, so the plaintext never leaves your machine.
|
|
2460
|
-
3. Stores only the ciphertext with `set_secret`, scoped to the component, never in the global `processEnv` tier.
|
|
2461
|
-
4. Grants the component permission to resolve it with `grant_secret`.
|
|
2462
|
-
5. Prints the `credentials` reference for the deploy to use.
|
|
2463
|
-
|
|
2464
|
-
Use a **fine-grained** personal access token with **Contents: Read-only** on that one repository. Avoid the `gh` CLI's session token: it typically carries `repo`, `read:org`, `gist`, and `workflow` scopes across your whole account. The sealed token is durable, and later deploys reuse it.
|
|
2611
|
+
`get_job` returns status `COMPLETE`, `IN_PROGRESS`, or `ERROR`. It answers only on the node that ran the job. A deploy-only role that waits for rolling deploys must include `get_job` in its operations allowlist.
|
|
2612
|
+
- `list_deployments` returns records newest first. Filter by `project` and `status`; omit `limit` to return every matching record. A deploy in progress reads `pending`, or `loading`, until its `completed_at` is set.
|
|
2465
2613
|
|
|
2466
2614
|
#### Examples
|
|
2467
2615
|
|
|
2468
|
-
|
|
2616
|
+
##### Full rolling deploy from a pinned tag
|
|
2469
2617
|
|
|
2470
2618
|
```bash
|
|
2619
|
+
# Authenticate once
|
|
2471
2620
|
harper login https://my-cluster.harperdbcloud.com
|
|
2621
|
+
|
|
2622
|
+
# Deploy a pinned release with rolling restart
|
|
2472
2623
|
harper deploy \
|
|
2473
2624
|
project=my-app \
|
|
2474
|
-
|
|
2625
|
+
ref=v1.2.0 \
|
|
2475
2626
|
target=https://my-cluster.harperdbcloud.com \
|
|
2476
2627
|
restart=rolling \
|
|
2477
|
-
|
|
2478
|
-
# Poll the node that received the deploy until the job is COMPLETE or ERROR
|
|
2479
|
-
harper get_job id=<restartJobId from deploy.json> json=true
|
|
2628
|
+
replicated=true
|
|
2480
2629
|
```
|
|
2481
2630
|
|
|
2482
|
-
|
|
2631
|
+
##### Stage a build, then activate in a maintenance window
|
|
2483
2632
|
|
|
2484
|
-
```
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2633
|
+
```json
|
|
2634
|
+
{
|
|
2635
|
+
"operation": "deploy_component",
|
|
2636
|
+
"project": "my-app",
|
|
2637
|
+
"package": "npm:@my-org/my-app@1.2.3",
|
|
2638
|
+
"activate": false
|
|
2639
|
+
}
|
|
2488
2640
|
```
|
|
2489
2641
|
|
|
2490
|
-
|
|
2642
|
+
Response:
|
|
2491
2643
|
|
|
2492
|
-
```
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2644
|
+
```json
|
|
2645
|
+
{
|
|
2646
|
+
"deployment_id": "a3f8c2d1-...",
|
|
2647
|
+
"message": "Staged: my-app. Deploy it with deploy_component deployment_id=a3f8c2d1-..."
|
|
2648
|
+
}
|
|
2496
2649
|
```
|
|
2497
2650
|
|
|
2498
|
-
|
|
2651
|
+
Activate later:
|
|
2499
2652
|
|
|
2500
|
-
```
|
|
2501
|
-
|
|
2653
|
+
```json
|
|
2654
|
+
{
|
|
2655
|
+
"operation": "deploy_component",
|
|
2656
|
+
"project": "my-app",
|
|
2657
|
+
"deployment_id": "a3f8c2d1-...",
|
|
2658
|
+
"restart": true
|
|
2659
|
+
}
|
|
2502
2660
|
```
|
|
2503
2661
|
|
|
2504
|
-
|
|
2662
|
+
##### Roll back to a previous release
|
|
2505
2663
|
|
|
2506
|
-
```
|
|
2507
|
-
|
|
2508
|
-
|
|
2664
|
+
```json
|
|
2665
|
+
{
|
|
2666
|
+
"operation": "list_deployments",
|
|
2667
|
+
"project": "my-app"
|
|
2668
|
+
}
|
|
2669
|
+
```
|
|
2670
|
+
|
|
2671
|
+
Then:
|
|
2509
2672
|
|
|
2510
|
-
|
|
2511
|
-
|
|
2673
|
+
```json
|
|
2674
|
+
{
|
|
2675
|
+
"operation": "deploy_component",
|
|
2676
|
+
"project": "my-app",
|
|
2677
|
+
"deployment_id": "<id of the deployment you want back>",
|
|
2678
|
+
"restart": true
|
|
2679
|
+
}
|
|
2512
2680
|
```
|
|
2513
2681
|
|
|
2682
|
+
##### Rejected canary — error response shape
|
|
2683
|
+
|
|
2684
|
+
```json
|
|
2685
|
+
{
|
|
2686
|
+
"error": "my-app was not deployed: release 9b2e... failed to load in its canary worker: my-app.jsResource: ... Deployment a3f8c2d1..., the release it replaced, is live again. No other node received it. Deploy a fix, or activate deployment 9b2e... again once the cause is fixed.",
|
|
2687
|
+
"deployment_id": "9b2e...",
|
|
2688
|
+
"certification": {
|
|
2689
|
+
"status": "rejected",
|
|
2690
|
+
"reason": "my-app.jsResource: ...",
|
|
2691
|
+
"failures": [
|
|
2692
|
+
{ "key": "my-app.jsResource", "name": "ResourceLoadError", "message": "...", "stack": "..." }
|
|
2693
|
+
],
|
|
2694
|
+
"restored": "a3f8c2d1...",
|
|
2695
|
+
"failed_closed": false
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
```
|
|
2699
|
+
|
|
2700
|
+
#### Notes
|
|
2701
|
+
|
|
2702
|
+
- `harper login` stores a token; subsequent `harper deploy` calls to the same target need no credentials. See [creating-a-fabric-account-and-cluster.md](creating-a-fabric-account-and-cluster.md) for cluster setup.
|
|
2703
|
+
- For GitHub Actions, prefer OIDC workload identity over stored tokens. See [deploying-from-ci.md](deploying-from-ci.md).
|
|
2704
|
+
- `restart=rolling` and `activate=false` cannot be combined.
|
|
2705
|
+
- `deployment_id` activation takes no `package`, `payload`, `credentials`, install options, `urlPath`, or `host` — supplying them is rejected.
|
|
2706
|
+
- `file:` directory sources (`package: file:<dir>`) cannot be staged and are not kept as rollback targets.
|
|
2707
|
+
- `stagingRetention.maxCount` (default `5`) caps staged builds and kept replaced releases together per component.
|
|
2708
|
+
- `get_job` on a `restartJobId` answers only on the node that received the deploy; any other node returns an empty array.
|
|
2709
|
+
- A rolling deploy's record reads `rolling_out` until the job has visited every node; filter `list_deployments` without `status` and check each record's `status` for a finished value (`success`, `partial`, `failed`, `staged`). `completed_at` alone does not confirm a finished state, since a `rolling_out` record already has it.
|
|
2710
|
+
- `enc:v1:` envelopes store only ciphertext — plaintext never leaves your machine and never appears in operation logs or replication.
|
|
2711
|
+
- Tags used in `refs/tags` are resolved to a commit SHA before the deploy is sent; a tag that moves mid-deploy cannot cause nodes to run different code.
|
|
2712
|
+
- `super_user` is required to run `harper deploy setup=true`.
|
|
2713
|
+
|
|
2514
2714
|
### 4.2 Deploying a Harper Application from GitHub Actions with OIDC
|
|
2515
2715
|
|
|
2516
|
-
Instructions for the agent to configure and
|
|
2716
|
+
Instructions for the agent to configure and use OIDC trusted publishing so a GitHub Actions workflow can deploy to Harper with no stored credential.
|
|
2517
2717
|
|
|
2518
2718
|
#### When to Use
|
|
2519
2719
|
|
|
2520
|
-
Apply this rule when
|
|
2720
|
+
Apply this rule when wiring up a GitHub Actions workflow to deploy a Harper application without storing a `HARPER_CLI_REFRESH_TOKEN` or any other durable credential in CI. Use it both for the one-command setup path (branch-triggered deploys) and for the manual policy path (tag-triggered releases). Also apply it when diagnosing a rejected OIDC exchange or revoking pipeline access.
|
|
2521
2721
|
|
|
2522
2722
|
#### How It Works
|
|
2523
2723
|
|
|
2524
|
-
##### One-Command Setup (
|
|
2724
|
+
##### One-Command Setup (branch-triggered deploys)
|
|
2525
2725
|
|
|
2526
|
-
1. **Run setup once** from the project checkout, signed in as a `super_user`:
|
|
2726
|
+
1. **Run setup once** from the project's checkout, signed in as a `super_user`:
|
|
2527
2727
|
|
|
2528
2728
|
```bash
|
|
2529
|
-
harper login https://my-cluster.example.com:9925
|
|
2530
2729
|
harper deploy setup=true provider=github-actions project=my-app
|
|
2531
2730
|
```
|
|
2532
2731
|
|
|
2533
|
-
If your workflow file is not `.github/workflows/deploy.yaml`, pass `workflow
|
|
2732
|
+
If your workflow file is not `.github/workflows/deploy.yaml`, pass `workflow=`:
|
|
2534
2733
|
|
|
2535
2734
|
```bash
|
|
2536
2735
|
harper deploy setup=true provider=github-actions project=my-app workflow=.github/workflows/deploy.yml
|
|
2537
2736
|
```
|
|
2538
2737
|
|
|
2539
|
-
2. **Understand what setup does** —
|
|
2540
|
-
- Resolves the GitHub repository's numeric
|
|
2541
|
-
-
|
|
2542
|
-
- Creates a role and a user named `<project>-ci-deploy` with permissions limited to `deploy_component`, `get_job`, and `registration_info`,
|
|
2543
|
-
- Reads the policy back and compares it claim by claim
|
|
2544
|
-
- Sets the repository's `HARPER_CLI_TARGET`
|
|
2738
|
+
2. **Understand what setup does** — it executes these steps automatically:
|
|
2739
|
+
- Resolves the GitHub repository's numeric `repository_id` from the `origin` remote (or `repo=<owner>/<name>`). Pass `repository_id=<id>` explicitly for private repos without `gh`.
|
|
2740
|
+
- Validates the workflow file: it must be able to run on `main` (or `branch=`), run a job in the `production` environment (`environment=`), and grant `id-token: write`. Setup stops before writing anything if the workflow could never match or never request a token.
|
|
2741
|
+
- Creates a role and a user named `<project>-ci-deploy` with permissions limited to `deploy_component`, `get_deployment`, `get_job`, and `registration_info`, plus the trust policy `github-actions-<project>`.
|
|
2742
|
+
- Reads the policy back and compares it claim by claim.
|
|
2743
|
+
- Sets the repository's `HARPER_CLI_TARGET` variable via `gh` if one does not already exist.
|
|
2545
2744
|
|
|
2546
|
-
3. **Handle idempotency**: setup only creates — it never
|
|
2745
|
+
3. **Handle idempotency**: setup only creates — it never overwrites. A role, user, or policy that already exists but differs stops setup before writing. To replace a policy:
|
|
2547
2746
|
|
|
2548
2747
|
```bash
|
|
2549
2748
|
harper drop_oidc_trust id=github-actions-<project>
|
|
2550
|
-
harper deploy setup=true provider=github-actions project=my-app
|
|
2551
2749
|
```
|
|
2552
2750
|
|
|
2553
|
-
|
|
2751
|
+
Then run setup again.
|
|
2554
2752
|
|
|
2555
|
-
|
|
2753
|
+
---
|
|
2556
2754
|
|
|
2557
|
-
|
|
2755
|
+
##### Manual Setup (tag-triggered deploys)
|
|
2756
|
+
|
|
2757
|
+
Use this path when the workflow deploys on version tags, because a branch pin cannot match a tag ref.
|
|
2758
|
+
|
|
2759
|
+
1. **Log in as `super_user`**:
|
|
2760
|
+
|
|
2761
|
+
```bash
|
|
2762
|
+
harper login https://my-cluster.example.com:9925
|
|
2763
|
+
```
|
|
2764
|
+
|
|
2765
|
+
2. **Create a deploy-only role and user** named `<project>-ci-deploy`:
|
|
2558
2766
|
|
|
2559
2767
|
```bash
|
|
2560
2768
|
harper add_role role=ci_deploy permission='{"operations":["deploy_component","get_job","get_deployment","registration_info"]}'
|
|
@@ -2562,12 +2770,12 @@ Use this path when deploying on version tags, because a branch-pinned policy can
|
|
|
2562
2770
|
```
|
|
2563
2771
|
|
|
2564
2772
|
- `deploy_component` — the deploy itself.
|
|
2565
|
-
- `
|
|
2566
|
-
- `
|
|
2567
|
-
- `registration_info` — version check
|
|
2773
|
+
- `get_deployment` — required for `harper deploy restart=rolling` to wait on every node; without it that wait fails with `403`.
|
|
2774
|
+
- `get_job` — required for the wait step; without it the wait step fails with `403`.
|
|
2775
|
+
- `registration_info` — version check before upload; omitting it logs a refused operation on every run.
|
|
2568
2776
|
- `restart_service` is not needed: Harper starts the rolling restart's job itself. The password is required to create the user but a GitHub Actions pipeline never uses it, so this command discards it.
|
|
2569
2777
|
|
|
2570
|
-
|
|
2778
|
+
3. **Add a trust policy** pinning `repository_id`, `workflow_path`, and `environment` (use this shape for tag-triggered workflows where `workflow_ref` cannot be pinned):
|
|
2571
2779
|
|
|
2572
2780
|
```bash
|
|
2573
2781
|
harper add_oidc_trust \
|
|
@@ -2579,45 +2787,40 @@ Use this path when deploying on version tags, because a branch-pinned policy can
|
|
|
2579
2787
|
```
|
|
2580
2788
|
|
|
2581
2789
|
Key claim rules:
|
|
2582
|
-
- `repository_id` is immutable
|
|
2583
|
-
- `workflow_path`
|
|
2790
|
+
- `repository_id` is immutable and survives renames. Look it up with `gh api repos/my-org/my-app --jq .id`.
|
|
2791
|
+
- `workflow_path` is derived from `workflow_ref` by removing the `@<ref>` suffix; format is `<owner>/<repo>/<path>`.
|
|
2584
2792
|
- `environment` is the ref gate. Without it, anyone who can push a branch can mint a token.
|
|
2585
2793
|
- `audience` must be the exact target URL with its port and a **trailing slash**.
|
|
2586
2794
|
- A workflow that deploys on a push to `main` can pin `workflow_ref` (`my-org/my-app/.github/workflows/deploy.yml@refs/heads/main`) instead, which gates the ref by itself.
|
|
2587
2795
|
|
|
2588
|
-
|
|
2796
|
+
4. **Configure the GitHub environment**: in **Settings → Environments → production**, set **Deployment branches and tags** to the `v*` tag pattern. Add required reviewers if a release needs approval.
|
|
2589
2797
|
|
|
2590
|
-
|
|
2798
|
+
5. **Store the target as a repository variable** (not a secret):
|
|
2591
2799
|
|
|
2592
2800
|
```bash
|
|
2593
2801
|
gh variable set HARPER_CLI_TARGET --repo my-org/my-app --body https://my-cluster.example.com:9925/
|
|
2594
2802
|
```
|
|
2595
2803
|
|
|
2804
|
+
---
|
|
2805
|
+
|
|
2596
2806
|
##### How OIDC Authentication Works in the Workflow
|
|
2597
2807
|
|
|
2598
|
-
|
|
2808
|
+
A job authenticates automatically when all four conditions hold:
|
|
2599
2809
|
|
|
2600
|
-
1. **The job
|
|
2601
|
-
2. **`HARPER_CLI_TARGET` is the policy's `audience`.** The CLI requests its token for the target it connects to, so the target must be the URL the policy names, with its port and trailing slash.
|
|
2602
|
-
3. **The run matches the policy's claims**: its
|
|
2810
|
+
1. **The job declares `permissions: id-token: write`.**
|
|
2811
|
+
2. **`HARPER_CLI_TARGET` is the policy's `audience`.** The CLI requests its token for the target it connects to, so the target must be the URL the policy names, with its port and trailing slash. It is not sensitive, so use `vars.HARPER_CLI_TARGET` rather than a secret.
|
|
2812
|
+
3. **The run matches the policy's claims**: its `repository_id`, `workflow_path`/`workflow_ref`, and `environment`.
|
|
2603
2813
|
4. **Nothing else is configured to authenticate.** `auth_username=`/`auth_password=`, credentials in the target URL, `HARPER_CLI_USERNAME`/`HARPER_CLI_PASSWORD`, `HARPER_CLI_REFRESH_TOKEN` or `HARPER_CLI_OPERATION_TOKEN`, and a saved `harper login` all outrank the exchange.
|
|
2604
2814
|
|
|
2605
|
-
|
|
2815
|
+
Nothing in the workflow names the policy. Every `harper` command in the job (`harper deploy`, `harper get_job`, etc.) authenticates through it automatically.
|
|
2606
2816
|
|
|
2607
2817
|
The exchange runs only when GitHub sets `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, which it does for a job that declares `permissions: id-token: write`. Without them, the CLI falls through to its other credential sources rather than reporting a failure.
|
|
2608
2818
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
```
|
|
2612
|
-
Requesting a GitHub Actions identity token for https://my-instance.harperdb.io:9925/...
|
|
2613
|
-
Authenticated as 'ci-deploy' via OIDC trust policy 'my-app-prod'.
|
|
2614
|
-
```
|
|
2615
|
-
|
|
2616
|
-
Diagnose a rejected exchange with `list_oidc_trust` (check for `invalid_reason` on each entry) and the instance's `oidc-trust` log. The server deliberately does not report which check failed.
|
|
2819
|
+
---
|
|
2617
2820
|
|
|
2618
2821
|
##### Policy Specificity for GitHub Actions
|
|
2619
2822
|
|
|
2620
|
-
For `https://token.actions.githubusercontent.com`,
|
|
2823
|
+
For `issuer=https://token.actions.githubusercontent.com`, every policy must satisfy all three requirements:
|
|
2621
2824
|
|
|
2622
2825
|
| Requirement | Satisfied by one of | Left open otherwise |
|
|
2623
2826
|
| ---------------------- | ------------------------------------------------------------------------ | ----------------------------------------------- |
|
|
@@ -2627,21 +2830,68 @@ For `https://token.actions.githubusercontent.com`, a policy must satisfy all thr
|
|
|
2627
2830
|
|
|
2628
2831
|
Additional constraints:
|
|
2629
2832
|
|
|
2630
|
-
- `repository_id` is preferred over `repository` — it is immutable and survives renames.
|
|
2631
|
-
- A tag-triggered release cannot pin `workflow_ref` since the tag is unknown when the policy is written. Pin `workflow_path` and gate on `environment`.
|
|
2632
2833
|
- `ref_type: tag` is not accepted as a ref gate, `sub` is not accepted as a pin, and `job_workflow_ref` pins the workflow without gating the ref. `pull_request_target` runs are denied unless the policy constrains `event_name`.
|
|
2633
2834
|
|
|
2634
|
-
|
|
2835
|
+
---
|
|
2836
|
+
|
|
2837
|
+
##### Diagnosing a Rejected Exchange
|
|
2838
|
+
|
|
2839
|
+
Run `list_oidc_trust` (requires `super_user`) to list all policies including disabled ones:
|
|
2840
|
+
|
|
2841
|
+
```json
|
|
2842
|
+
{ "operation": "list_oidc_trust" }
|
|
2843
|
+
```
|
|
2844
|
+
|
|
2845
|
+
`list_oidc_trust` returns `{ "policies": [ ... ] }`. Each entry includes `id`, `issuer`, `audience`, `claims`, `user`, `enabled`, and optionally `invalid_reason`. Check `invalid_reason` first when diagnosing a rejected exchange — it is set when the policy is malformed or its user was deleted or deactivated after the policy was written. The exchange itself will not report what failed.
|
|
2846
|
+
|
|
2847
|
+
Check the instance's `oidc-trust` log for server-side detail.
|
|
2848
|
+
|
|
2849
|
+
---
|
|
2635
2850
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
- **
|
|
2851
|
+
##### Revoking Pipeline Access
|
|
2852
|
+
|
|
2853
|
+
- **Stop future runs** — drop the policy:
|
|
2854
|
+
|
|
2855
|
+
```bash
|
|
2856
|
+
harper drop_oidc_trust id=github-actions-<project>
|
|
2857
|
+
```
|
|
2858
|
+
|
|
2859
|
+
This does not revoke tokens already issued (they are stateless JWTs valid for one hour).
|
|
2860
|
+
|
|
2861
|
+
- **Stop a token already in circulation** — deactivate the user:
|
|
2862
|
+
|
|
2863
|
+
```bash
|
|
2864
|
+
harper alter_user username=<project>-ci-deploy active=false
|
|
2865
|
+
```
|
|
2866
|
+
|
|
2867
|
+
Use both together when responding to a suspected compromise.
|
|
2868
|
+
|
|
2869
|
+
---
|
|
2870
|
+
|
|
2871
|
+
##### Providing a Durable Credential for a Private Source
|
|
2872
|
+
|
|
2873
|
+
Do not use the workflow's ephemeral `GITHUB_TOKEN` for Harper's stored credential — it expires when the job ends, and later installs (a new node joining, a reinstall after restore) would fail. Instead use a durable token:
|
|
2874
|
+
|
|
2875
|
+
- For a private repository: a fine-grained personal access token with **Contents: read-only** scoped to the application repository.
|
|
2876
|
+
- For GitHub Packages: a token with the `read:packages` scope.
|
|
2877
|
+
|
|
2878
|
+
Store it in Harper once from your machine:
|
|
2879
|
+
|
|
2880
|
+
```bash
|
|
2881
|
+
# Private git repository
|
|
2882
|
+
harper deploy setup=true provider=github project=my-app
|
|
2883
|
+
|
|
2884
|
+
# GitHub Packages
|
|
2885
|
+
harper deploy setup=true provider=npm project=my-app registry=https://npm.pkg.github.com scope=@my-org
|
|
2886
|
+
```
|
|
2887
|
+
|
|
2888
|
+
The name is derived from the component and the host: `deploy.my-app.git.github.com` for the repository, and `deploy.my-app.npm.pkg.github.com` for GitHub Packages. Reference it by that name in the workflow — the token never appears in your pipeline.
|
|
2639
2889
|
|
|
2640
2890
|
#### Examples
|
|
2641
2891
|
|
|
2642
2892
|
##### Path A: Deploy a Tagged Release (`v1.2.3`) from the Repository
|
|
2643
2893
|
|
|
2644
|
-
Push a semver tag (`v1.2.3`), and every node clones and installs that commit. `by_ref=true` deploys `git+https://github.com/<owner>/<repo>.git#<sha>`, where the SHA is the commit the tag points to (`GITHUB_SHA`), and every node clones that exact commit even if the tag
|
|
2894
|
+
Push a semver tag (`v1.2.3`), and every node clones and installs that commit. `by_ref=true` deploys `git+https://github.com/<owner>/<repo>.git#<sha>`, where the SHA is the commit the tag points to (`GITHUB_SHA`), and every node clones that exact commit even if the tag is moved later. If this repository needs a build step to be runnable, do not reach for `install_allow_scripts` — use Path B instead.
|
|
2645
2895
|
|
|
2646
2896
|
```yaml
|
|
2647
2897
|
# .github/workflows/deploy.yml
|
|
@@ -2671,6 +2921,8 @@ jobs:
|
|
|
2671
2921
|
run: harper deploy project=my-app by_ref=true restart=rolling json=true > deploy.json
|
|
2672
2922
|
- name: Wait for every node to take the release
|
|
2673
2923
|
run: |
|
|
2924
|
+
# From v5.4.0 a certified rolling deploy waited for every node itself.
|
|
2925
|
+
[ "$(jq -r '.rollout.status // empty' deploy.json)" != success ] || exit 0
|
|
2674
2926
|
JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
|
|
2675
2927
|
[ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
|
|
2676
2928
|
LAST="could not be read"
|
|
@@ -2691,14 +2943,17 @@ jobs:
|
|
|
2691
2943
|
Key details:
|
|
2692
2944
|
|
|
2693
2945
|
- **No secrets.** `HARPER_CLI_TARGET` is the only thing the workflow is given. `id-token: write` lets the CLI request an identity token, and `environment: production` puts the `environment` claim the trust policy requires into it.
|
|
2694
|
-
- **`persist-credentials: false`** keeps the job's `GITHUB_TOKEN` out of `.git/config
|
|
2946
|
+
- **`persist-credentials: false`** keeps the job's `GITHUB_TOKEN` out of `.git/config`, where `actions/checkout` otherwise leaves it for every later step.
|
|
2695
2947
|
- **`harper@^5.3`** pins the CLI's major version. 5.3 is the first CLI that authenticates with the identity token.
|
|
2696
2948
|
- **`replicated` isn't needed.** On Harper Fabric and Harper Pro, a deploy goes to every node unless you pass `replicated=false`. Each node fetches and installs the package itself.
|
|
2697
|
-
- **`restart=rolling`** restarts nodes one at a time. Use `restart=true` for a single-node or dev instance.
|
|
2698
|
-
- **`
|
|
2699
|
-
-
|
|
2700
|
-
-
|
|
2701
|
-
|
|
2949
|
+
- **`restart=rolling`** restarts nodes one at a time so the cluster keeps serving throughout. Use `restart=true` for a single-node or dev instance.
|
|
2950
|
+
- **`certification`**: on v5.4.0 and later, `restart=rolling` loads the release in a canary worker on each node before that node serves it, so a release that fails to load is rejected. The `certification` field in the deploy result reflects whether this ran.
|
|
2951
|
+
- **`restartJobId`**: present in the deploy JSON when a rolling job was started but the deploy did not wait for it itself. The wait step polls `.[0].status` from `get_job` until `COMPLETE` or `ERROR`.
|
|
2952
|
+
- **Load-balanced clusters**: the deployment record wait works through a load-balanced URL (the record replicates), but `get_job` polls do not — Harper does not replicate jobs. If your load balancer never routes back to the node that received the deploy, use one node's direct URL for both the deploy and the wait, and as the trust policy's `audience`.
|
|
2953
|
+
|
|
2954
|
+
For a private repository, add `credential=true` to the deploy command.
|
|
2955
|
+
|
|
2956
|
+
---
|
|
2702
2957
|
|
|
2703
2958
|
##### Path B: Build in CI, Publish to GitHub Packages, Deploy the Artifact
|
|
2704
2959
|
|
|
@@ -2767,6 +3022,7 @@ jobs:
|
|
|
2767
3022
|
restart=rolling json=true > deploy.json
|
|
2768
3023
|
- name: Wait for every node to take the release
|
|
2769
3024
|
run: |
|
|
3025
|
+
[ "$(jq -r '.rollout.status // empty' deploy.json)" != success ] || exit 0
|
|
2770
3026
|
JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
|
|
2771
3027
|
[ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
|
|
2772
3028
|
LAST="could not be read"
|
|
@@ -2784,56 +3040,28 @@ jobs:
|
|
|
2784
3040
|
exit 1
|
|
2785
3041
|
```
|
|
2786
3042
|
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
Credential roles:
|
|
3043
|
+
Split into two jobs so the job with `id-token: write` runs none of your build code. Only the `deploy` job names `environment: production`, so only its token carries the `environment` claim the trust policy requires.
|
|
2790
3044
|
|
|
2791
|
-
|
|
2792
|
-
- **Installing on Harper nodes** uses the durable `read:packages` token stored as `deploy.my-app.npm.pkg.github.com` — because installs can happen long after this workflow ends.
|
|
3045
|
+
---
|
|
2793
3046
|
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
```bash
|
|
2797
|
-
harper deploy setup=true provider=github project=my-app
|
|
2798
|
-
# or for GitHub Packages:
|
|
2799
|
-
harper deploy setup=true provider=npm project=my-app registry=https://npm.pkg.github.com scope=@my-org
|
|
2800
|
-
```
|
|
2801
|
-
|
|
2802
|
-
For a private repository, use a GitHub fine-grained personal access token with **Contents: read-only** scoped to the application repository. For GitHub Packages, use a token with the `read:packages` scope. Do not use the workflow's ephemeral `GITHUB_TOKEN` for the durable credential — it expires when the job ends.
|
|
2803
|
-
|
|
2804
|
-
##### Check the Deployment Record
|
|
3047
|
+
##### Checking the Deployment Record
|
|
2805
3048
|
|
|
2806
3049
|
```bash
|
|
2807
3050
|
harper get_deployment deployment_id="$(jq -r .deployment_id deploy.json)" json=true
|
|
2808
3051
|
```
|
|
2809
3052
|
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
##### List and Inspect Trust Policies
|
|
2813
|
-
|
|
2814
|
-
```bash
|
|
2815
|
-
harper list_oidc_trust
|
|
2816
|
-
```
|
|
2817
|
-
|
|
2818
|
-
Returns `{ "policies": [ ... ] }`. Each entry includes `id`, `issuer`, `audience`, `claims`, `user`, `enabled`, and optionally `invalid_reason`. Check `invalid_reason` first when diagnosing a rejected exchange — it is set when the policy is malformed or its user was deleted or deactivated.
|
|
2819
|
-
|
|
2820
|
-
##### Drop a Trust Policy
|
|
3053
|
+
Use `list_deployments` to see the history of what was deployed and when, and filter by commit with `sha`.
|
|
2821
3054
|
|
|
2822
|
-
|
|
2823
|
-
harper drop_oidc_trust id=github-actions-<project>
|
|
2824
|
-
```
|
|
3055
|
+
- **Go back to a previous release** by activating its `deployment_id`, with no rebuild or reinstall: `harper deploy project=my-app deployment_id=<id> restart=rolling`. To deploy a release that is no longer kept, re-run the deploy with its tag or version.
|
|
2825
3056
|
|
|
2826
3057
|
#### Notes
|
|
2827
3058
|
|
|
2828
|
-
- `
|
|
2829
|
-
-
|
|
2830
|
-
-
|
|
2831
|
-
-
|
|
2832
|
-
-
|
|
2833
|
-
-
|
|
2834
|
-
- For tag-triggered workflows, `provider=github-actions` setup is not sufficient — it pins a branch. Write the policy manually using `add_oidc_trust` with `workflow_path` and `environment` claims.
|
|
2835
|
-
- `token.actions.githubusercontent.com` is the required `issuer` value for GitHub Actions OIDC policies.
|
|
2836
|
-
- Git-host credentials require git ≥ 2.31 on the Harper nodes and are not supported on Windows nodes. Registry credentials have no such constraints.
|
|
3059
|
+
- `audience` in `add_oidc_trust` must include the port and a **trailing slash** — e.g., `https://my-cluster.example.com:9925/`. A URL without the trailing slash will not match.
|
|
3060
|
+
- `HARPER_CLI_REFRESH_TOKEN` and any other configured credential (`auth_username=`/`auth_password=`, `HARPER_CLI_USERNAME`/`HARPER_CLI_PASSWORD`, `HARPER_CLI_OPERATION_TOKEN`, a saved `harper login`) outrank the OIDC exchange. Remove stored credentials from the runner before enabling OIDC.
|
|
3061
|
+
- `list_oidc_trust`, `drop_oidc_trust`, and `alter_user` all require `super_user`.
|
|
3062
|
+
- `drop_oidc_trust` does not revoke already-issued operation tokens (one-hour JWTs). Pair it with `alter_user username=<project>-ci-deploy active=false` to stop tokens already in circulation.
|
|
3063
|
+
- `ref_type: tag` is not accepted as a ref gate. Gate tag-triggered workflows with `environment` instead, and set **Deployment branches and tags** to `v*` in the environment's settings.
|
|
3064
|
+
- Every step in a job with `id-token: write` can request an identity token — GitHub gives each one `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`. Run none of the repository's code in that job.
|
|
2837
3065
|
- See [deploying-to-harper-fabric.md](deploying-to-harper-fabric.md) for cluster-level deployment context.
|
|
2838
3066
|
|
|
2839
3067
|
### 4.3 Creating a Harper Fabric Account and Cluster
|
|
@@ -2982,6 +3210,8 @@ don't have `deploy:setup-ci`. To move one over:
|
|
|
2982
3210
|
|
|
2983
3211
|
Instructions for the agent to follow when serving web content from Harper.
|
|
2984
3212
|
|
|
3213
|
+
**Instead of:** Nginx, Vercel, or Netlify for the frontend; a separate Vite dev server. Use this rule rather than adding that service. See `harper-architecture`.
|
|
3214
|
+
|
|
2985
3215
|
#### When to Use
|
|
2986
3216
|
|
|
2987
3217
|
Use this skill when you need to serve a frontend (HTML, CSS, JS, or a React/Vue app) directly from your Harper instance — either plain static files or an integrated Vite app with hot module replacement (HMR) in development and a real production build when deployed.
|
|
@@ -3089,6 +3319,8 @@ On deploy the plugin runs `vite build` at startup (and rebuilds when `files` cha
|
|
|
3089
3319
|
|
|
3090
3320
|
Instructions for the agent to follow when implementing logging in Harper applications, including direct logger usage, tagged loggers, and console capture behavior.
|
|
3091
3321
|
|
|
3322
|
+
**Instead of:** Separate log shippers for app logs (pino or winston transports). Use this rule rather than adding that service. See `harper-architecture`.
|
|
3323
|
+
|
|
3092
3324
|
#### When to Use
|
|
3093
3325
|
|
|
3094
3326
|
Apply this rule when writing any JavaScript component, plugin, or resource that needs to emit structured log entries, filter logs by component, or capture existing `console.log` output into Harper's log system. Use it whenever you need to understand log levels, log entry format, or the `logger` global API.
|
|
@@ -3245,6 +3477,8 @@ Tagged entries appear in `hdb.log` with the tag in the header:
|
|
|
3245
3477
|
|
|
3246
3478
|
Instructions for the agent to follow when loading environment variables from `.env` files into a Harper application using the `loadEnv` plugin.
|
|
3247
3479
|
|
|
3480
|
+
**Instead of:** `dotenv`. Use this rule rather than adding that service. See `harper-architecture`.
|
|
3481
|
+
|
|
3248
3482
|
#### When to Use
|
|
3249
3483
|
|
|
3250
3484
|
Apply this rule when a Harper application needs to supply secrets, API endpoints, or other configuration values to component code via `process.env` without hardcoding them. Use this whenever a `.env` file must be loaded at application startup.
|