@harperfast/template-react-ts-studio 1.12.8 → 1.12.9
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 +514 -445
- package/.agents/skills/harper-best-practices/rules/automatic-apis.md +96 -93
- package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +84 -58
- package/.agents/skills/harper-best-practices/rules/extending-tables.md +27 -35
- package/.agents/skills/harper-best-practices/rules/handling-binary-data.md +40 -39
- package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +93 -37
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +68 -66
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +24 -28
- package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +98 -105
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +28 -3
- package/.agents/skills/harper-mcp/AGENTS.md +138 -26
- package/.agents/skills/harper-mcp/rules/enabling-mcp.md +140 -28
- package/agent/skills/harper-best-practices/AGENTS.md +514 -445
- package/agent/skills/harper-best-practices/rules/automatic-apis.md +96 -93
- package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +84 -58
- package/agent/skills/harper-best-practices/rules/extending-tables.md +27 -35
- package/agent/skills/harper-best-practices/rules/handling-binary-data.md +40 -39
- package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +93 -37
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +68 -66
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +24 -28
- package/agent/skills/harper-best-practices/rules/v5-upgrade.md +98 -105
- package/agent/skills/harper-best-practices/rules.manifest.yaml +28 -3
- package/agent/skills/harper-mcp/AGENTS.md +138 -26
- package/agent/skills/harper-mcp/rules/enabling-mcp.md +140 -28
- package/package.json +1 -1
- package/skills-lock.json +2 -2
|
@@ -9,21 +9,30 @@ metadata:
|
|
|
9
9
|
- reference/v5/database/schema.md#Overview
|
|
10
10
|
- reference/v5/database/schema.md#Type Directives
|
|
11
11
|
- reference/v5/database/schema.md#Field Directives
|
|
12
|
-
sourceCommit:
|
|
13
|
-
inputHash:
|
|
12
|
+
sourceCommit: 2cfb81318f17e0aca2d600109e6ad813d2d0443e
|
|
13
|
+
inputHash: a644f4ea2f289f77
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
# Schema Design
|
|
16
|
+
# Schema Design Tooling
|
|
17
17
|
|
|
18
|
-
Instructions for the agent to follow when designing Harper
|
|
18
|
+
Instructions for the agent to follow when designing Harper schemas, applying core directives, and configuring GraphQL tooling.
|
|
19
19
|
|
|
20
20
|
## When to Use
|
|
21
21
|
|
|
22
|
-
Apply this rule when creating or modifying Harper schema files (`.graphql`), configuring
|
|
22
|
+
Apply this rule when creating or modifying Harper schema files (`.graphql`), 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.
|
|
23
23
|
|
|
24
24
|
## How It Works
|
|
25
25
|
|
|
26
|
-
1. **
|
|
26
|
+
1. **Register the schema file** in the component's `config.yaml` using the `graphqlSchema` plugin key:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
graphqlSchema:
|
|
30
|
+
files: 'schema.graphql'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Both plugins and applications can specify schemas.
|
|
34
|
+
|
|
35
|
+
2. **Mark types as tables** using `@table`. The type name becomes the table name by default:
|
|
27
36
|
|
|
28
37
|
```graphql
|
|
29
38
|
type Dog @table {
|
|
@@ -34,79 +43,71 @@ Apply this rule when creating or modifying Harper schema files (`.graphql`), con
|
|
|
34
43
|
}
|
|
35
44
|
```
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
3. **Designate a primary key** on every table using `@primaryKey`. Primary keys must be unique; duplicate inserts are rejected. If no primary key is provided on insert, Harper auto-generates one:
|
|
47
|
+
- `String` or `ID` → UUID string
|
|
48
|
+
- `Int`, `Long`, or `Any` → auto-incrementing integer
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
graphqlSchema:
|
|
41
|
-
files: 'schema.graphql'
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Both plugins and applications can specify schemas.
|
|
50
|
+
Use `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
4. **Index fields for querying** using `@indexed`. Required for filtering by an attribute in REST queries, SQL, or NoSQL operations:
|
|
47
53
|
|
|
48
54
|
```graphql
|
|
49
|
-
type
|
|
55
|
+
type Breed @table {
|
|
50
56
|
id: Long @primaryKey
|
|
57
|
+
name: String @indexed
|
|
51
58
|
}
|
|
52
59
|
```
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
If the field value is an array, each element is individually indexed. Null values are indexed by default.
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
| -------------------- | --------- | ----------------------------- | --------------------------------------------------------------- |
|
|
58
|
-
| `table` | `String` | type name | Override the table name |
|
|
59
|
-
| `database` | `String` | `"data"` | Database to place the table in |
|
|
60
|
-
| `expiration` | `Int` | — | Seconds until a record goes stale |
|
|
61
|
-
| `eviction` | `Int` | `0` | Additional seconds after `expiration` before physical removal |
|
|
62
|
-
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
|
|
63
|
-
| `replicate` | `Boolean` | `true` | Enable replication of this table |
|
|
64
|
-
| `cacheControl` | `String` | — | `Cache-Control` header for anonymous GET/HEAD 200/304 responses |
|
|
65
|
-
| `randomAccessFields` | `Boolean` | `storage.randomAccessFields` | Pin this table's record encoding |
|
|
66
|
-
|
|
67
|
-
4. **Designate a primary key** on every table using `@primaryKey`. Primary keys must be unique; duplicate inserts are rejected. If no primary key is provided on insert, Harper auto-generates one:
|
|
68
|
-
- **UUID string** — when type is `String` or `ID`
|
|
69
|
-
- **Auto-incrementing integer** — when type is `Int`, `Long`, or `Any`
|
|
70
|
-
|
|
71
|
-
Use `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
|
|
63
|
+
5. **Expose tables as REST/MQTT endpoints** using `@export`. The optional `name` parameter sets the URL path segment; without it, the type name is used:
|
|
72
64
|
|
|
73
65
|
```graphql
|
|
74
|
-
type
|
|
66
|
+
type MyTable @table @export(name: "my-table") {
|
|
75
67
|
id: Long @primaryKey
|
|
76
|
-
name: String
|
|
77
68
|
}
|
|
78
69
|
```
|
|
79
70
|
|
|
80
|
-
|
|
71
|
+
`@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 but does not protect the data.
|
|
72
|
+
|
|
73
|
+
6. **Configure `@table` arguments** to control database placement, expiration, replication, and caching behavior. Key arguments:
|
|
74
|
+
|
|
75
|
+
| Argument | Type | Default | Description |
|
|
76
|
+
| -------------------- | --------- | ----------------------------- | -------------------------------------------------------------- |
|
|
77
|
+
| `table` | `String` | type name | Override the table name |
|
|
78
|
+
| `database` | `String` | `"data"` | Database to place the table in |
|
|
79
|
+
| `expiration` | `Int` | — | Seconds until a record goes stale |
|
|
80
|
+
| `eviction` | `Int` | `0` | Additional seconds after `expiration` before physical removal |
|
|
81
|
+
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
|
|
82
|
+
| `replicate` | `Boolean` | `true` | Enable replication of this table |
|
|
83
|
+
| `cacheControl` | `String` | — | `Cache-Control` header on anonymous GET/HEAD 200/304 responses |
|
|
84
|
+
| `randomAccessFields` | `Boolean` | `storage.randomAccessFields` | Pin this table's record encoding |
|
|
85
|
+
|
|
86
|
+
7. **Seal a type** with `@sealed` to prevent records from including properties beyond those declared:
|
|
81
87
|
|
|
82
88
|
```graphql
|
|
83
|
-
type
|
|
89
|
+
type StrictRecord @table @sealed {
|
|
84
90
|
id: Long @primaryKey
|
|
85
|
-
|
|
86
|
-
price: Float @indexed
|
|
91
|
+
name: String
|
|
87
92
|
}
|
|
88
93
|
```
|
|
89
94
|
|
|
90
|
-
|
|
95
|
+
8. **Use timestamp directives** for automatic record lifecycle tracking:
|
|
96
|
+
- `@createdTime` — assigns Unix epoch milliseconds on record creation
|
|
97
|
+
- `@updatedTime` — assigns Unix epoch milliseconds on each update
|
|
98
|
+
- `@expiresAt` — marks a field as the record's absolute expiration time (Unix epoch milliseconds); authoritative over the table-level `expiration` default
|
|
99
|
+
|
|
100
|
+
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]`):
|
|
91
101
|
|
|
92
102
|
```graphql
|
|
93
|
-
type
|
|
103
|
+
type Document @table {
|
|
94
104
|
id: Long @primaryKey
|
|
105
|
+
text: String
|
|
106
|
+
embedding: [Float] @embed(source: "text", model: "default")
|
|
95
107
|
}
|
|
96
108
|
```
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
7. **Apply additional type directives** as needed:
|
|
101
|
-
- `@sealed` — prevents records from including properties beyond those declared in the schema.
|
|
102
|
-
- `@hidden` — suppresses the type from MCP tool descriptors and the OpenAPI document. Does not restrict data access.
|
|
103
|
-
|
|
104
|
-
8. **Apply field directives** for computed and lifecycle behavior:
|
|
105
|
-
- `@createdTime` — assigns Unix epoch milliseconds on record creation.
|
|
106
|
-
- `@updatedTime` — assigns Unix epoch milliseconds on each update.
|
|
107
|
-
- `@expiresAt` — marks a field as the record's absolute expiration time (Unix epoch milliseconds); authoritative over the table-level `expiration` default.
|
|
108
|
-
- `@embed` — computes an embedding vector when the source field is written (requires `source` and `model` arguments; field type must be `[Float]`).
|
|
109
|
-
- `@hidden` (field) — suppresses the field from generated specs and MCP tool schemas; does not restrict data access.
|
|
110
|
+
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.
|
|
110
111
|
|
|
111
112
|
## Examples
|
|
112
113
|
|
|
@@ -136,7 +137,7 @@ type WeatherCache @table(expiration: 300, eviction: 3300, scanInterval: 600) {
|
|
|
136
137
|
}
|
|
137
138
|
```
|
|
138
139
|
|
|
139
|
-
**Exported table with cache control:**
|
|
140
|
+
**Exported table with public cache control:**
|
|
140
141
|
|
|
141
142
|
```graphql
|
|
142
143
|
type Product @table(cacheControl: "public, max-age=60") @export {
|
|
@@ -146,14 +147,12 @@ type Product @table(cacheControl: "public, max-age=60") @export {
|
|
|
146
147
|
}
|
|
147
148
|
```
|
|
148
149
|
|
|
149
|
-
**Table with
|
|
150
|
+
**Table with multiple `@table` arguments combined:**
|
|
150
151
|
|
|
151
152
|
```graphql
|
|
152
153
|
type Event @table(database: "analytics", expiration: 86400) {
|
|
153
154
|
id: Long @primaryKey
|
|
154
155
|
name: String @indexed
|
|
155
|
-
createdAt: Long @createdTime
|
|
156
|
-
updatedAt: Long @updatedTime
|
|
157
156
|
}
|
|
158
157
|
```
|
|
159
158
|
|
|
@@ -167,12 +166,15 @@ type Session @table {
|
|
|
167
166
|
}
|
|
168
167
|
```
|
|
169
168
|
|
|
170
|
-
**
|
|
169
|
+
**Table with timestamp tracking and indexed fields:**
|
|
171
170
|
|
|
172
171
|
```graphql
|
|
173
|
-
type
|
|
172
|
+
type Order @table(database: "commerce") @export {
|
|
174
173
|
id: Long @primaryKey
|
|
175
|
-
|
|
174
|
+
userId: String @indexed
|
|
175
|
+
status: String @indexed
|
|
176
|
+
createdAt: Long @createdTime
|
|
177
|
+
updatedAt: Long @updatedTime
|
|
176
178
|
}
|
|
177
179
|
```
|
|
178
180
|
|
|
@@ -185,10 +187,10 @@ graphqlSchema:
|
|
|
185
187
|
|
|
186
188
|
## Notes
|
|
187
189
|
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
191
|
-
- `@
|
|
192
|
-
-
|
|
193
|
-
-
|
|
194
|
-
- `randomAccessFields`
|
|
190
|
+
- Use unique `database` names in plugins and applications to avoid table naming collisions, since all tables default to the `"data"` database.
|
|
191
|
+
- Disabling replication (`replicate: false`) and re-enabling it later will not catch up on writes made while replication was disabled.
|
|
192
|
+
- `cacheControl` emits the header only on anonymous (unauthenticated) GET/HEAD `200`/`304` responses. Authenticated responses receive `Cache-Control: private, no-cache` regardless of the declaration. The header is never emitted on `401` responses.
|
|
193
|
+
- `@expiresAt` requires an absolute Unix epoch millisecond timestamp, not a duration. Negative values are ignored and fall back to the table default. A full-record `put` that omits the field clears it; a `patch` preserves it.
|
|
194
|
+
- Eviction removes non-indexed record data but does not remove a record from its secondary indexes; indexes remain functional for evicted records, with full records fetched on demand.
|
|
195
|
+
- `scanInterval` is clock-aligned to the server's local timezone, not startup-aligned — the server's startup time does not affect when eviction runs.
|
|
196
|
+
- `randomAccessFields` pins the table's encoding at creation time. Editing the argument later does not repin an existing table. Omit it to follow the global `storage.randomAccessFields` setting.
|
|
@@ -8,8 +8,8 @@ metadata:
|
|
|
8
8
|
- reference/v5/database/api.md#Streaming
|
|
9
9
|
- reference/v5/database/api.md#`BlobOptions`
|
|
10
10
|
- reference/v5/database/api.md#Blob Coercion
|
|
11
|
-
sourceCommit:
|
|
12
|
-
inputHash:
|
|
11
|
+
sourceCommit: 2cfb81318f17e0aca2d600109e6ad813d2d0443e
|
|
12
|
+
inputHash: 753b85cbdcef9188
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
# Using the Blob Data Type
|
|
@@ -18,11 +18,11 @@ Instructions for the agent to follow when storing and retrieving large binary co
|
|
|
18
18
|
|
|
19
19
|
## When to Use
|
|
20
20
|
|
|
21
|
-
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
|
|
21
|
+
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.
|
|
22
22
|
|
|
23
23
|
## How It Works
|
|
24
24
|
|
|
25
|
-
1. **Declare a `Blob` field in your schema**: Add a field typed as `Blob` to your
|
|
25
|
+
1. **Declare a `Blob` field in your schema**: Add a field typed as `Blob` to your table definition.
|
|
26
26
|
|
|
27
27
|
```graphql
|
|
28
28
|
type MyTable @table {
|
|
@@ -31,14 +31,14 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
2. **Create and store a blob with `createBlob()`**: Pass a buffer or stream to `createBlob()`, then `put` the record.
|
|
34
|
+
2. **Create and store a blob with `createBlob()`**: Pass a buffer, string, or stream to `createBlob()`, then `put` the record.
|
|
35
35
|
|
|
36
36
|
```javascript
|
|
37
37
|
let blob = createBlob(largeBuffer);
|
|
38
38
|
await MyTable.put({ id: 'my-record', data: blob });
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
3. **Retrieve blob data using standard Web API methods**:
|
|
41
|
+
3. **Retrieve blob data using standard Web API methods**: Use `.bytes()`, `.text()`, or `.stream()` on the retrieved field.
|
|
42
42
|
|
|
43
43
|
```javascript
|
|
44
44
|
let record = await MyTable.get('my-record');
|
|
@@ -47,7 +47,7 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
47
47
|
let stream = record.data.stream(); // ReadableStream
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
4. **Use `saveBeforeCommit` when full write must
|
|
50
|
+
4. **Use `saveBeforeCommit` when full write must complete before commit**: By default, blobs are not ACID-compliant — a record can reference a blob before it is fully written. Set `saveBeforeCommit: true` to block the transaction until the blob is fully saved.
|
|
51
51
|
|
|
52
52
|
```javascript
|
|
53
53
|
let blob = createBlob(stream, { saveBeforeCommit: true });
|
|
@@ -55,13 +55,14 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
55
55
|
// put() resolves only after blob is fully written and record is committed
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
5. **Register an error handler when returning a blob via REST**:
|
|
58
|
+
5. **Register an error handler when returning a blob via REST**: Handle interrupted streams to avoid stale references.
|
|
59
59
|
|
|
60
60
|
```javascript
|
|
61
61
|
export class MyEndpoint extends MyTable {
|
|
62
62
|
static async get(target) {
|
|
63
|
-
const record = super.get(target);
|
|
64
|
-
let blob = record
|
|
63
|
+
const record = await super.get(target);
|
|
64
|
+
let blob = record?.data;
|
|
65
|
+
if (!blob) return record;
|
|
65
66
|
blob.on('error', () => {
|
|
66
67
|
MyTable.invalidate(target);
|
|
67
68
|
});
|
|
@@ -70,19 +71,15 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
70
71
|
}
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
6. **
|
|
74
|
+
6. **Pass `BlobOptions` to control storage behavior**: `createBlob()` accepts an options object as its second argument.
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| `
|
|
82
|
-
| `size` | `number` | `undefined` | Size of the data in bytes, if known ahead of time. Otherwise inferred from a buffer or determined as a stream completes. |
|
|
83
|
-
| `saveBeforeCommit` | `boolean` | `false` | Wait until the blob is fully written before the transaction commits. |
|
|
84
|
-
| `compress` | `boolean` | `false` | Compress the stored data with deflate. |
|
|
85
|
-
| `flush` | `boolean` | `false` | Flush the file to disk after writing, before the `createBlob` promise chain resolves. |
|
|
76
|
+
| Option | Type | Default | Description |
|
|
77
|
+
| ------------------ | --------- | ----------- | ----------------------------------------------------------------------------------- |
|
|
78
|
+
| `type` | `string` | `undefined` | MIME type (e.g., `image/jpeg`). Used when serving HTTP and readable via `blob.type` |
|
|
79
|
+
| `size` | `number` | `undefined` | Size in bytes if known ahead of time; otherwise inferred |
|
|
80
|
+
| `saveBeforeCommit` | `boolean` | `false` | Wait until blob is fully written before the transaction commits |
|
|
81
|
+
| `compress` | `boolean` | `false` | Compress stored data with deflate |
|
|
82
|
+
| `flush` | `boolean` | `false` | Flush file to disk after writing, before the `createBlob` promise chain resolves |
|
|
86
83
|
|
|
87
84
|
## Examples
|
|
88
85
|
|
|
@@ -97,15 +94,13 @@ await Photo.put({ id, data: blob });
|
|
|
97
94
|
|
|
98
95
|
```javascript
|
|
99
96
|
let blob = createBlob(incomingStream);
|
|
100
|
-
// blob exists, but data is still streaming to storage
|
|
101
97
|
await MyTable.put({ id: 'my-record', data: blob });
|
|
102
98
|
|
|
103
99
|
let record = await MyTable.get('my-record');
|
|
104
|
-
// blob data is accessible as it arrives
|
|
105
100
|
let outgoingStream = record.data.stream();
|
|
106
101
|
```
|
|
107
102
|
|
|
108
|
-
**
|
|
103
|
+
**Wait for full write before commit:**
|
|
109
104
|
|
|
110
105
|
```javascript
|
|
111
106
|
let blob = createBlob(stream, { saveBeforeCommit: true });
|
|
@@ -114,6 +109,7 @@ await MyTable.put({ id: 'my-record', data: blob });
|
|
|
114
109
|
|
|
115
110
|
## Notes
|
|
116
111
|
|
|
117
|
-
- `Blob`
|
|
118
|
-
-
|
|
119
|
-
-
|
|
112
|
+
- `Blob` implements the Web API `Blob` interface. All standard methods — `.text()`, `.arrayBuffer()`, `.stream()`, `.slice()`, and `.bytes()` — are available on retrieved blob fields.
|
|
113
|
+
- Blobs are stored **separately from the record**, not within it. If you need the binary data to be a true ACID-committed part of the record, use a `Bytes` field instead.
|
|
114
|
+
- Any string or buffer assigned to a `Blob`-typed field in a `put`, `patch`, or `publish` is automatically coerced to a `Blob` — manual `createBlob()` calls are not required in those cases.
|
|
115
|
+
- Use `saveBeforeCommit: true` whenever downstream consumers must not read a partially written blob.
|
|
@@ -7,60 +7,82 @@ metadata:
|
|
|
7
7
|
mode: generate
|
|
8
8
|
sources:
|
|
9
9
|
- release-notes/v5-lincoln/v5-migration.md
|
|
10
|
-
sourceCommit:
|
|
11
|
-
inputHash:
|
|
10
|
+
sourceCommit: 2cfb81318f17e0aca2d600109e6ad813d2d0443e
|
|
11
|
+
inputHash: 66ca7a9e265d006e
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# v5 Upgrade: Breaking Changes and Migration Guide
|
|
15
15
|
|
|
16
|
-
Instructions for the agent to
|
|
16
|
+
Instructions for the agent to follow when migrating a Harper application to v5, covering all breaking changes and required code updates.
|
|
17
17
|
|
|
18
18
|
## When to Use
|
|
19
19
|
|
|
20
|
-
Apply this rule when upgrading an existing Harper application to v5, when encountering runtime errors
|
|
20
|
+
Apply this rule when upgrading an existing Harper application from v4 to v5, when encountering runtime errors after upgrading, or when reviewing application code for v5 compatibility. Every breaking change listed here must be addressed before the application will behave correctly under v5.
|
|
21
21
|
|
|
22
22
|
## How It Works
|
|
23
23
|
|
|
24
|
-
1. **Update the package import from `harperdb`
|
|
24
|
+
1. **Update the package import**: Replace all imports from `'harperdb'` with `from 'harper'`.
|
|
25
25
|
|
|
26
26
|
```javascript
|
|
27
27
|
import { tables } from 'harper';
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
2. **Enable
|
|
30
|
+
2. **Enable install scripts if needed**: Harper v5 runs `npm install` with `--ignore-scripts` by default. If your application requires install scripts (e.g., to compile native binaries), set `allowInstallScripts` in your deployment options.
|
|
31
31
|
|
|
32
|
-
3. **Update `Table.get` usage
|
|
33
|
-
- Replace direct property mutation:
|
|
32
|
+
3. **Update `Table.get` usage**: `Table.get` now returns a plain frozen record object, not a table class instance. The `wasLoadedFromSource()` method no longer exists on the returned object. Replace it with `loadedFromSource` on the `RequestTarget`:
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
```javascript
|
|
35
|
+
// Before
|
|
36
|
+
const record = await Table.get(id);
|
|
37
|
+
if (record.wasLoadedFromSource()) {
|
|
38
|
+
// record was loaded from origin (not cache)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
```javascript
|
|
43
|
+
// After
|
|
44
|
+
import { getContext } from 'harper';
|
|
45
|
+
const target = new RequestTarget();
|
|
46
|
+
target.id = id;
|
|
47
|
+
const record = await Table.get(target);
|
|
48
|
+
if (target.loadedFromSource) {
|
|
49
|
+
// record was loaded from origin (not cache)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
const target = new RequestTarget();
|
|
44
|
-
target.id = id;
|
|
45
|
-
const record = await Table.get(target);
|
|
46
|
-
if (target.loadedFromSource) {
|
|
47
|
-
// record was loaded from origin (not cache)
|
|
48
|
-
}
|
|
49
|
-
```
|
|
53
|
+
The record objects do have `getUpdatedTime` and `getExpiresAt` methods available.
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
4. **Handle frozen records**: The record object returned by `Table.get` is frozen — you cannot mutate it directly. Copy it before modifying:
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
```javascript
|
|
58
|
+
// Before
|
|
59
|
+
const record = await Table.get(id);
|
|
60
|
+
record.property = 'changed';
|
|
61
|
+
```
|
|
54
62
|
|
|
55
63
|
```javascript
|
|
56
|
-
|
|
64
|
+
// After
|
|
65
|
+
let record = await Table.get(id);
|
|
66
|
+
record = { ...record, property: 'changed' };
|
|
57
67
|
```
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
5. **Register allowed spawn commands via `allowedSpawnCommands`**: `spawn` and `execFile` may only launch executables listed in `applications.allowedSpawnCommands` in `harper-config.yaml`. Only the first token of the command is matched. `exec` is not usable through the substituted module, and `execSync` always throws.
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
applications:
|
|
73
|
+
allowedSpawnCommands:
|
|
74
|
+
- npm
|
|
75
|
+
- node
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Additionally, `spawn`, `execFile`, and `fork` now require a `name` property in the `options` argument to prevent process multiplication across threads.
|
|
79
|
+
|
|
80
|
+
6. **Update transaction and context usage**: Harper v5 uses asynchronous context tracking. `Table.get` and other calls now automatically inherit the current transaction. Code that previously omitted context to bypass a transaction will no longer work as expected. Explicitly commit the transaction or wrap calls in a new transaction to read updated data:
|
|
60
81
|
|
|
61
82
|
```javascript
|
|
62
83
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
63
84
|
import { getContext, transaction } from 'harper';
|
|
85
|
+
|
|
64
86
|
class MyResource {
|
|
65
87
|
static async get(target) {
|
|
66
88
|
await getContext().transaction.commit();
|
|
@@ -72,79 +94,70 @@ Apply this rule when upgrading an existing Harper application to v5, when encoun
|
|
|
72
94
|
}
|
|
73
95
|
```
|
|
74
96
|
|
|
75
|
-
|
|
97
|
+
Use `getContext` (exported from `'harper'`) to access the current transaction anywhere without passing context explicitly.
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
applications:
|
|
79
|
-
allowedSpawnCommands:
|
|
80
|
-
- npm
|
|
81
|
-
- node
|
|
82
|
-
```
|
|
99
|
+
7. **Replace `blob.save()`**: The `blob.save()` method has been removed. Use the `saveBeforeCommit` flag in the options passed to the `Blob` constructor instead.
|
|
83
100
|
|
|
84
|
-
|
|
101
|
+
8. **Handle response `headers`**: If you return an object from a REST method with a `headers` property, Harper v5 will use it as the response headers. Ensure any object you return that incidentally has a `headers` property is intentional.
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
9. **Configure the module loader**: Harper v5 loads application modules through Node.js's VM module API. Control this with the `moduleLoader` setting:
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
| Value | Behavior |
|
|
106
|
+
| -------------------- | ------------------------------------------------ |
|
|
107
|
+
| `vm-current-context` | Default. Shares intrinsics with Harper. |
|
|
108
|
+
| `vm` | Separate context and intrinsics per application. |
|
|
109
|
+
| `native` | Standard `import()`, no application context. |
|
|
110
|
+
| `compartment` | SES Compartments; advanced and heavier. |
|
|
89
111
|
|
|
90
|
-
|
|
112
|
+
The default (`vm-current-context`) avoids `instanceof` failures for values crossing the application/Harper boundary. Choose `vm` only if you need separate per-application intrinsics. Use `native` if the VM loader causes compatibility problems you cannot otherwise resolve.
|
|
113
|
+
|
|
114
|
+
To disable the VM loader entirely and restore pre-v5 behavior:
|
|
91
115
|
|
|
92
116
|
```yaml
|
|
93
117
|
applications:
|
|
94
|
-
|
|
95
|
-
moduleLoader: vm-current-context
|
|
96
|
-
dependencyLoader: auto
|
|
97
|
-
allowedDirectory: app
|
|
98
|
-
allowedSpawnCommands:
|
|
99
|
-
- npm
|
|
100
|
-
- node
|
|
118
|
+
moduleLoader: native
|
|
101
119
|
```
|
|
102
120
|
|
|
103
|
-
|
|
121
|
+
Note: `logger` and per-app `config` are not available in `native` mode.
|
|
104
122
|
|
|
105
|
-
|
|
106
|
-
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
-
| `vm-current-context` | Default. VM loader in Harper's own context; shares intrinsics with Harper. Best compatibility. |
|
|
108
|
-
| `vm` | VM loader in a separate per-application context with its own intrinsics. Stronger isolation but may cause `instanceof` failures. |
|
|
109
|
-
| `native` | Standard Node.js `import()`. No VM loader; application-specific context (`logger`, `config`) unavailable. |
|
|
110
|
-
| `compartment` | SES Compartment-based loading. For specialized sandboxing only. |
|
|
123
|
+
If the goal is only to fix package compatibility while keeping application context for first-party code, `dependencyLoader: native` is a narrower option — it uses native loading only for npm packages while keeping the VM loader for application source files.
|
|
111
124
|
|
|
112
|
-
|
|
125
|
+
10. **Handle intrinsic `lockdown`**: The default `lockdown` mode (`freeze-after-load`) freezes JavaScript intrinsics (`Object`, `Array`, `Promise`, `Map`, `Set`, etc.) after all application code loads. Any code or dependency that modifies intrinsic prototypes at runtime will throw a `TypeError`. If a dependency requires a temporary workaround, set:
|
|
113
126
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
| `ses` | Full SES lockdown via the `ses` package. Strictest. |
|
|
119
|
-
| `none` | No lockdown. Use as a temporary workaround only. |
|
|
127
|
+
```yaml
|
|
128
|
+
applications:
|
|
129
|
+
lockdown: none
|
|
130
|
+
```
|
|
120
131
|
|
|
121
|
-
|
|
132
|
+
11. **Check `allowedDirectory` in production**: In production, applications can only load modules from within their own directory tree (`allowedDirectory: app`). If your application loads files from outside its directory, set:
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
```yaml
|
|
135
|
+
applications:
|
|
136
|
+
allowedDirectory: any
|
|
137
|
+
```
|
|
127
138
|
|
|
128
139
|
## Examples
|
|
129
140
|
|
|
130
|
-
|
|
141
|
+
### Full `harper-config.yaml` module loading block
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
applications:
|
|
145
|
+
lockdown: freeze-after-load
|
|
146
|
+
moduleLoader: vm-current-context
|
|
147
|
+
dependencyLoader: auto
|
|
148
|
+
allowedDirectory: app
|
|
149
|
+
allowedSpawnCommands:
|
|
150
|
+
- npm
|
|
151
|
+
- node
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Updated import and context access
|
|
131
155
|
|
|
132
156
|
```javascript
|
|
133
|
-
import {
|
|
134
|
-
import { getContext, transaction } from 'harper';
|
|
135
|
-
|
|
136
|
-
class MyResource {
|
|
137
|
-
static async get(target) {
|
|
138
|
-
await getContext().transaction.commit();
|
|
139
|
-
while ((await transaction(() => Table.get(target))).status !== 'ready') {
|
|
140
|
-
await delay(100);
|
|
141
|
-
}
|
|
142
|
-
return Table.get(target);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
157
|
+
import { tables, getContext, transaction } from 'harper';
|
|
145
158
|
```
|
|
146
159
|
|
|
147
|
-
|
|
160
|
+
### Checking `loadedFromSource` after `Table.get`
|
|
148
161
|
|
|
149
162
|
```javascript
|
|
150
163
|
const target = new RequestTarget();
|
|
@@ -155,37 +168,17 @@ if (target.loadedFromSource) {
|
|
|
155
168
|
}
|
|
156
169
|
```
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
```yaml
|
|
161
|
-
applications:
|
|
162
|
-
lockdown: freeze-after-load
|
|
163
|
-
moduleLoader: vm-current-context
|
|
164
|
-
dependencyLoader: auto
|
|
165
|
-
allowedDirectory: app
|
|
166
|
-
allowedSpawnCommands:
|
|
167
|
-
- npm
|
|
168
|
-
- node
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**Restricting allowed built-in modules:**
|
|
171
|
+
### Copying a frozen record before mutation
|
|
172
172
|
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
- fs
|
|
177
|
-
- path
|
|
178
|
-
- http
|
|
173
|
+
```javascript
|
|
174
|
+
let record = await Table.get(id);
|
|
175
|
+
record = { ...record, property: 'changed' };
|
|
179
176
|
```
|
|
180
177
|
|
|
181
178
|
## Notes
|
|
182
179
|
|
|
183
|
-
-
|
|
184
|
-
- `
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
- `
|
|
188
|
-
- The `headers` property on a returned REST response object is used as response headers.
|
|
189
|
-
- Under `lockdown: ses`, the constrained `fetch` applies only in `vm` mode. In `vm-current-context` and `native` modes, application code uses the standard global `fetch`.
|
|
190
|
-
- In production, `allowedDirectory: app` is the default; modules outside the application directory tree will throw. Set `allowedDirectory: any` only if legitimately required.
|
|
191
|
-
- `dependencyLoader: native` is a narrower option than `moduleLoader: native` — it uses native loading only for npm packages while keeping the VM loader for first-party application source files.
|
|
180
|
+
- The `logger` exported from `'harper'` is tagged with the application name when using the VM module loader. It is not available in `native` mode.
|
|
181
|
+
- `instanceof` checks for values crossing the application/Harper boundary will fail when using `moduleLoader: vm` (separate intrinsics). Use `vm-current-context` (the default) to avoid this.
|
|
182
|
+
- `import()` behavior depends on the `moduleLoader` setting. In `native` mode, standard Node.js `import()` is used with no application context.
|
|
183
|
+
- Automatic context tracking simplifies code but requires explicit `commit()` or new `transaction()` calls when you need to observe data written within the same transaction.
|
|
184
|
+
- Use `getContext` from `'harper'` rather than passing context manually through every call — this is the recommended pattern in v5.
|