@harperfast/template-vue-ts-studio 1.10.0 → 1.10.2
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 +228 -166
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +65 -27
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +139 -131
- package/.agents/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +19 -18
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +26 -1
- package/agent/skills/harper-best-practices/AGENTS.md +228 -166
- package/agent/skills/harper-best-practices/rules/custom-resources.md +65 -27
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +139 -131
- package/agent/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +19 -18
- package/agent/skills/harper-best-practices/rules.manifest.yaml +26 -1
- package/package.json +1 -1
- package/skills-lock.json +1 -1
|
@@ -510,15 +510,15 @@ let closeMatches = Document.search({
|
|
|
510
510
|
|
|
511
511
|
### 1.5 Using the Blob Data Type
|
|
512
512
|
|
|
513
|
-
Instructions for the agent to follow when storing and retrieving large binary content using
|
|
513
|
+
Instructions for the agent to follow when storing and retrieving large binary content using the `Blob` data type in Harper.
|
|
514
514
|
|
|
515
515
|
#### When to Use
|
|
516
516
|
|
|
517
|
-
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
|
|
517
|
+
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.md](handling-binary-data.md) for broader binary data guidance.
|
|
518
518
|
|
|
519
519
|
#### How It Works
|
|
520
520
|
|
|
521
|
-
1. **Declare a `Blob` field in your schema**: Add a field typed as `Blob` to
|
|
521
|
+
1. **Declare a `Blob` field in your schema**: Add a field typed as `Blob` to your `@table` type.
|
|
522
522
|
|
|
523
523
|
```graphql
|
|
524
524
|
type MyTable @table {
|
|
@@ -527,14 +527,14 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
527
527
|
}
|
|
528
528
|
```
|
|
529
529
|
|
|
530
|
-
2. **Create a blob with `createBlob()`**: Pass a buffer
|
|
530
|
+
2. **Create and store a blob with `createBlob()`**: Pass a buffer or stream to `createBlob()`, then `put` the record.
|
|
531
531
|
|
|
532
532
|
```javascript
|
|
533
533
|
let blob = createBlob(largeBuffer);
|
|
534
534
|
await MyTable.put({ id: 'my-record', data: blob });
|
|
535
535
|
```
|
|
536
536
|
|
|
537
|
-
3. **
|
|
537
|
+
3. **Retrieve blob data using standard Web API methods**: The `Blob` type implements the Web API `Blob` interface. Use `.bytes()`, `.text()`, `.arrayBuffer()`, `.stream()`, or `.slice()` as needed.
|
|
538
538
|
|
|
539
539
|
```javascript
|
|
540
540
|
let record = await MyTable.get('my-record');
|
|
@@ -543,7 +543,7 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
543
543
|
let stream = record.data.stream(); // ReadableStream
|
|
544
544
|
```
|
|
545
545
|
|
|
546
|
-
4. **Use `saveBeforeCommit`
|
|
546
|
+
4. **Use `saveBeforeCommit` when full write must precede commit**: By default, `Blob` is 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.
|
|
547
547
|
|
|
548
548
|
```javascript
|
|
549
549
|
let blob = createBlob(stream, { saveBeforeCommit: true });
|
|
@@ -551,7 +551,7 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
551
551
|
// put() resolves only after blob is fully written and record is committed
|
|
552
552
|
```
|
|
553
553
|
|
|
554
|
-
5. **Register an error handler when returning a blob via REST**: Interrupted streams must be handled explicitly
|
|
554
|
+
5. **Register an error handler when returning a blob via REST**: Interrupted streams must be handled explicitly.
|
|
555
555
|
|
|
556
556
|
```javascript
|
|
557
557
|
export class MyEndpoint extends MyTable {
|
|
@@ -566,15 +566,17 @@ Apply this rule when a schema field needs to store large binary content such as
|
|
|
566
566
|
}
|
|
567
567
|
```
|
|
568
568
|
|
|
569
|
-
6. **Rely on automatic coercion where applicable**: When a field is typed as `Blob` in the schema, any string or buffer assigned via `put`, `patch`, or `publish` is automatically coerced to a `Blob
|
|
569
|
+
6. **Rely on automatic coercion where applicable**: When a field is typed as `Blob` in the schema, any string or buffer assigned via `put`, `patch`, or `publish` is automatically coerced to a `Blob` — no manual `createBlob()` call is needed in those cases.
|
|
570
570
|
|
|
571
|
-
##### `BlobOptions`
|
|
571
|
+
##### `BlobOptions` reference
|
|
572
|
+
|
|
573
|
+
Pass an options object as the second argument to `createBlob()`.
|
|
572
574
|
|
|
573
575
|
| Option | Type | Default | Description |
|
|
574
576
|
| ------------------ | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
575
577
|
| `type` | `string` | `undefined` | MIME type to associate with the blob (e.g., `image/jpeg`). Readable via `blob.type` and used when serving HTTP. |
|
|
576
578
|
| `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. |
|
|
577
|
-
| `saveBeforeCommit` | `boolean` | `false` | Wait
|
|
579
|
+
| `saveBeforeCommit` | `boolean` | `false` | Wait until the blob is fully written before the transaction commits. |
|
|
578
580
|
| `compress` | `boolean` | `false` | Compress the stored data with deflate. |
|
|
579
581
|
| `flush` | `boolean` | `false` | Flush the file to disk after writing, before the `createBlob` promise chain resolves. |
|
|
580
582
|
|
|
@@ -587,7 +589,7 @@ let blob = createBlob(imageBuffer, { type: 'image/jpeg' });
|
|
|
587
589
|
await Photo.put({ id, data: blob });
|
|
588
590
|
```
|
|
589
591
|
|
|
590
|
-
**Stream
|
|
592
|
+
**Stream a blob in as it streams out (low-latency passthrough):**
|
|
591
593
|
|
|
592
594
|
```javascript
|
|
593
595
|
let blob = createBlob(incomingStream);
|
|
@@ -599,7 +601,7 @@ let record = await MyTable.get('my-record');
|
|
|
599
601
|
let outgoingStream = record.data.stream();
|
|
600
602
|
```
|
|
601
603
|
|
|
602
|
-
**
|
|
604
|
+
**Guarantee full write before commit using `saveBeforeCommit`:**
|
|
603
605
|
|
|
604
606
|
```javascript
|
|
605
607
|
let blob = createBlob(stream, { saveBeforeCommit: true });
|
|
@@ -608,10 +610,9 @@ await MyTable.put({ id: 'my-record', data: blob });
|
|
|
608
610
|
|
|
609
611
|
#### Notes
|
|
610
612
|
|
|
611
|
-
- `Blob` stores data separately from the record
|
|
612
|
-
- All standard Web API `Blob` methods
|
|
613
|
-
-
|
|
614
|
-
- Always attach an `error` handler on blobs returned as HTTP response bodies to handle interrupted streams.
|
|
613
|
+
- `Blob` stores data separately from the record. If you need the binary data to be a true, ACID-committed part of the record, use a `Bytes` field instead.
|
|
614
|
+
- All standard Web API `Blob` methods — `.text()`, `.arrayBuffer()`, `.stream()`, `.slice()`, and `.bytes()` — are available on retrieved blob fields.
|
|
615
|
+
- Without `saveBeforeCommit: true`, blobs are **not** ACID-compliant by default; a record can reference a blob before it is fully written to storage.
|
|
615
616
|
|
|
616
617
|
### 1.6 Handling Binary Data
|
|
617
618
|
|
|
@@ -1350,17 +1351,17 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
|
|
|
1350
1351
|
|
|
1351
1352
|
#### When to Use
|
|
1352
1353
|
|
|
1353
|
-
Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering
|
|
1354
|
+
Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed schema, or when a specific URL shape is required.
|
|
1354
1355
|
|
|
1355
1356
|
#### How It Works
|
|
1356
1357
|
|
|
1357
|
-
1. **Import `Resource` from the `harper` package
|
|
1358
|
+
1. **Import `Resource` from `harper`**: Always import from the `harper` package rather than relying on globals.
|
|
1358
1359
|
|
|
1359
1360
|
```javascript
|
|
1360
1361
|
import { tables, Resource } from 'harper';
|
|
1361
1362
|
```
|
|
1362
1363
|
|
|
1363
|
-
2. **Define a class that `extends Resource`**:
|
|
1364
|
+
2. **Define a class that `extends Resource`**: Implement HTTP methods as `static` methods. Each method receives a `target` object.
|
|
1364
1365
|
|
|
1365
1366
|
```javascript
|
|
1366
1367
|
export class CustomEndpoint extends Resource {
|
|
@@ -1372,7 +1373,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1372
1373
|
}
|
|
1373
1374
|
```
|
|
1374
1375
|
|
|
1375
|
-
3. **
|
|
1376
|
+
3. **Use `async` static methods for external calls**: Await fetch or other async operations inside `static` handlers.
|
|
1376
1377
|
|
|
1377
1378
|
```javascript
|
|
1378
1379
|
export class MyExternalData extends Resource {
|
|
@@ -1390,29 +1391,46 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1390
1391
|
}
|
|
1391
1392
|
```
|
|
1392
1393
|
|
|
1393
|
-
4. **
|
|
1394
|
+
4. **Export the class to create an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
|
|
1395
|
+
|
|
1396
|
+
| Export form | URL | Notes |
|
|
1397
|
+
| ------------------------------------------- | --------------- | --------------------------------------------------------------- |
|
|
1398
|
+
| `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment. Case-sensitive. |
|
|
1399
|
+
| `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
|
|
1400
|
+
| `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
|
|
1401
|
+
| `export { Foo as '/widget/:id' }` | `/widget/:id` | Rename the export to set the path directly. |
|
|
1402
|
+
| `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
|
|
1403
|
+
| `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
|
|
1404
|
+
|
|
1405
|
+
URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
|
|
1406
|
+
|
|
1407
|
+
5. **Declare path parameters with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
|
|
1408
|
+
|
|
1409
|
+
```javascript
|
|
1410
|
+
export class Widget extends Resource {
|
|
1411
|
+
static path = '/widget/:id/action/:action';
|
|
1412
|
+
static get(target) {
|
|
1413
|
+
return { id: target.id, action: target.action };
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
```
|
|
1394
1417
|
|
|
1395
|
-
|
|
1396
|
-
| ---------------------------------------- | --------------- | --------------------------------------------------------------- |
|
|
1397
|
-
| `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment. |
|
|
1398
|
-
| `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
|
|
1399
|
-
| `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
|
|
1400
|
-
| `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration; useful when the path is dynamic. |
|
|
1418
|
+
A `static path` takes precedence over the export name. A leading `/` makes the path root-relative (top-level). A leading `./` or bare name resolves relative to the component directory.
|
|
1401
1419
|
|
|
1402
|
-
|
|
1420
|
+
6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` when the path cannot be known at export time.
|
|
1403
1421
|
|
|
1404
1422
|
```javascript
|
|
1405
1423
|
server.resources.set('my-path', Foo);
|
|
1406
1424
|
```
|
|
1407
1425
|
|
|
1408
|
-
|
|
1426
|
+
7. **Optionally source a table from a custom resource**: Use the resource as a caching layer for a local table.
|
|
1409
1427
|
```javascript
|
|
1410
1428
|
tables.MyCache.sourcedFrom(MyExternalData);
|
|
1411
1429
|
```
|
|
1412
1430
|
|
|
1413
1431
|
#### Examples
|
|
1414
1432
|
|
|
1415
|
-
|
|
1433
|
+
##### External API wrapper with GET and PUT
|
|
1416
1434
|
|
|
1417
1435
|
```javascript
|
|
1418
1436
|
import { tables, Resource } from 'harper';
|
|
@@ -1435,29 +1453,50 @@ export class MyExternalData extends Resource {
|
|
|
1435
1453
|
tables.MyCache.sourcedFrom(MyExternalData);
|
|
1436
1454
|
```
|
|
1437
1455
|
|
|
1438
|
-
|
|
1456
|
+
##### Path parameters with `static path`
|
|
1457
|
+
|
|
1458
|
+
```javascript
|
|
1459
|
+
import { Resource } from 'harper';
|
|
1460
|
+
|
|
1461
|
+
export class Widget extends Resource {
|
|
1462
|
+
// GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
|
|
1463
|
+
static path = '/widget/:id/action/:action';
|
|
1464
|
+
static get(target) {
|
|
1465
|
+
return { id: target.id, action: target.action };
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
export class Files extends Resource {
|
|
1470
|
+
// GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
|
|
1471
|
+
static path = '/files/*rest';
|
|
1472
|
+
static get(target) {
|
|
1473
|
+
return { path: target.rest };
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
##### Programmatic registration
|
|
1439
1479
|
|
|
1440
1480
|
```javascript
|
|
1441
1481
|
import { Resource } from 'harper';
|
|
1442
1482
|
|
|
1443
|
-
export class
|
|
1483
|
+
export class Foo extends Resource {
|
|
1444
1484
|
static get(target) {
|
|
1445
|
-
return {
|
|
1446
|
-
data: doSomething(),
|
|
1447
|
-
};
|
|
1485
|
+
return { data: doSomething() };
|
|
1448
1486
|
}
|
|
1449
1487
|
}
|
|
1450
1488
|
|
|
1451
|
-
server.resources.set('my-path',
|
|
1489
|
+
server.resources.set('my-path', Foo);
|
|
1452
1490
|
```
|
|
1453
1491
|
|
|
1454
1492
|
#### Notes
|
|
1455
1493
|
|
|
1456
|
-
-
|
|
1457
|
-
-
|
|
1458
|
-
-
|
|
1459
|
-
-
|
|
1460
|
-
-
|
|
1494
|
+
- A bare `*` wildcard (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
|
|
1495
|
+
- Resolution order: exact/static paths always win over parameterized ones. Among parameterized routes, more specific paths win — a literal segment beats `:param`, which beats `*`, compared left to right.
|
|
1496
|
+
- Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as `{param}` URI templates.
|
|
1497
|
+
- If a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
|
|
1498
|
+
- Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
|
|
1499
|
+
- Harper runs as a single process — `tables`, `databases`, and other APIs are the same live, process-wide objects regardless of which component accesses them.
|
|
1461
1500
|
|
|
1462
1501
|
### 3.2 Extending Tables
|
|
1463
1502
|
|
|
@@ -1565,175 +1604,170 @@ if (!authorized) {
|
|
|
1565
1604
|
|
|
1566
1605
|
### 3.3 Programmatic Table Requests
|
|
1567
1606
|
|
|
1568
|
-
Instructions for the agent to
|
|
1607
|
+
Instructions for the agent to interact with Harper tables programmatically using the `tables` object and its query API.
|
|
1569
1608
|
|
|
1570
1609
|
#### When to Use
|
|
1571
1610
|
|
|
1572
|
-
|
|
1611
|
+
Apply this rule when writing server-side Harper code that reads from or writes to tables directly — for example, in request handlers, background jobs, or SSR entry points — instead of going through the REST API. Use it whenever you need to construct queries with `conditions`, paginate results, select specific fields, or perform CRDT-safe mutations with `addTo`.
|
|
1573
1612
|
|
|
1574
1613
|
#### How It Works
|
|
1575
1614
|
|
|
1576
|
-
1. **
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
3. **Use Updatable Records for Atomic Ops**: Call `update(id)` to get a reference, then use `addTo` or `subtractFrom` for atomic increments/decrements:
|
|
1583
|
-
```typescript
|
|
1584
|
-
const stats = await tables.Stats.update('daily');
|
|
1585
|
-
stats.addTo('viewCount', 1);
|
|
1586
|
-
```
|
|
1587
|
-
4. **Search and Stream**: Use `search(query)` for efficient streaming of large result sets:
|
|
1588
|
-
```typescript
|
|
1589
|
-
for await (const record of tables.MyTable.search({ conditions: [...] })) {
|
|
1590
|
-
// process record
|
|
1591
|
-
}
|
|
1615
|
+
1. **Import `tables`**: Import from the `harper` package. Each table defined in `schema.graphql` with `@table` is available as a named property.
|
|
1616
|
+
|
|
1617
|
+
```javascript
|
|
1618
|
+
import { tables } from 'harper';
|
|
1619
|
+
const { Product } = tables;
|
|
1620
|
+
// same as: databases.data.Product
|
|
1592
1621
|
```
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1622
|
+
|
|
1623
|
+
2. **Define your schema**: Declare tables in `schema.graphql` using `@table` and `@primaryKey`.
|
|
1624
|
+
|
|
1625
|
+
```graphql
|
|
1626
|
+
# schema.graphql
|
|
1627
|
+
type Product @table {
|
|
1628
|
+
id: Long @primaryKey
|
|
1629
|
+
name: String
|
|
1630
|
+
price: Float
|
|
1598
1631
|
}
|
|
1599
1632
|
```
|
|
1600
|
-
6. **Publish Events**: Use `publish(id, message)` to trigger subscriptions without necessarily persisting data.
|
|
1601
1633
|
|
|
1602
|
-
|
|
1634
|
+
3. **Create and mutate records**: Use `create`, `patch`, `get`, and `update` on the table class.
|
|
1603
1635
|
|
|
1604
|
-
|
|
1636
|
+
```javascript
|
|
1637
|
+
// Create a new record (id auto-generated)
|
|
1638
|
+
const created = await Product.create({ name: 'Shirt', price: 9.5 });
|
|
1605
1639
|
|
|
1606
|
-
|
|
1640
|
+
// Modify the record
|
|
1641
|
+
await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
|
|
1607
1642
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
| `value` | The value to compare against |
|
|
1612
|
-
| `comparator` | One of the comparator strings below (default: `equals`) |
|
|
1613
|
-
| `operator` | `and` (default) or `or` — applies to a nested `conditions` block |
|
|
1614
|
-
| `conditions` | Nested array of condition objects for complex AND/OR logic |
|
|
1643
|
+
// Retrieve by primary key
|
|
1644
|
+
const record = await Product.get(created.id);
|
|
1645
|
+
```
|
|
1615
1646
|
|
|
1616
|
-
|
|
1647
|
+
4. **Query with `search(` and `conditions`**: Pass a query object to `search()` to filter records. Iterate the async result.
|
|
1617
1648
|
|
|
1618
|
-
|
|
1649
|
+
```javascript
|
|
1650
|
+
const query = {
|
|
1651
|
+
conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
|
|
1652
|
+
};
|
|
1653
|
+
for await (const record of Product.search(query)) {
|
|
1654
|
+
// ...
|
|
1655
|
+
}
|
|
1656
|
+
```
|
|
1619
1657
|
|
|
1620
|
-
|
|
1621
|
-
| -------------------- | ---------------------------------------------------------- |
|
|
1622
|
-
| `equals` | Exact match (default) |
|
|
1623
|
-
| `not_equal` | Not equal |
|
|
1624
|
-
| `greater_than` | `>` |
|
|
1625
|
-
| `greater_than_equal` | `>=` |
|
|
1626
|
-
| `less_than` | `<` |
|
|
1627
|
-
| `less_than_equal` | `<=` |
|
|
1628
|
-
| `starts_with` | String starts with value |
|
|
1629
|
-
| `contains` | String contains value |
|
|
1630
|
-
| `ends_with` | String ends with value |
|
|
1631
|
-
| `between` | Value is between two bounds (pass `value` as `[min, max]`) |
|
|
1658
|
+
5. **Use `select` to shape results**: Pass a `select` array to return only specific properties, including nested relationship fields.
|
|
1632
1659
|
|
|
1633
|
-
|
|
1660
|
+
```javascript
|
|
1661
|
+
const book = await Book.get({ id: 42, select: ['id', 'title', 'author'] });
|
|
1662
|
+
book.author.name; // full related Author record
|
|
1634
1663
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
| `sort` | Object with `attribute`, `descending` (bool), and optional `next` for secondary sort |
|
|
1664
|
+
// Partial related record
|
|
1665
|
+
const book = await Book.get({
|
|
1666
|
+
id: 42,
|
|
1667
|
+
select: ['id', 'title', { name: 'author', select: ['name'] }],
|
|
1668
|
+
});
|
|
1669
|
+
```
|
|
1642
1670
|
|
|
1643
|
-
|
|
1671
|
+
6. **Use `addTo` for concurrent-safe increments**: Call `addTo` on a mutable resource instance obtained via `update()`. This uses CRDT incrementation, safe across threads and nodes.
|
|
1644
1672
|
|
|
1645
|
-
|
|
1673
|
+
```javascript
|
|
1674
|
+
static async post(target, data) {
|
|
1675
|
+
const record = await this.update(target.id);
|
|
1676
|
+
record.addTo('quantity', -1); // decrement safely across nodes
|
|
1677
|
+
}
|
|
1678
|
+
```
|
|
1646
1679
|
|
|
1647
|
-
|
|
1648
|
-
for await (const record of tables.Product.search({
|
|
1649
|
-
conditions: [{ attribute: 'price', comparator: 'less_than', value: 100 }],
|
|
1650
|
-
limit: 20,
|
|
1651
|
-
})) { ... }
|
|
1652
|
-
```
|
|
1680
|
+
7. **Scope destructive operations carefully**: `update`, `patch`, and `delete` operate directly on stored data. Always use specific `conditions`, validate the affected set before writing, and gate behind authorization controls.
|
|
1653
1681
|
|
|
1654
|
-
|
|
1682
|
+
#### Examples
|
|
1655
1683
|
|
|
1656
|
-
|
|
1657
|
-
for await (const record of tables.Product.search({
|
|
1658
|
-
conditions: [
|
|
1659
|
-
{ attribute: 'price', comparator: 'less_than', value: 100 },
|
|
1660
|
-
{
|
|
1661
|
-
operator: 'or',
|
|
1662
|
-
conditions: [
|
|
1663
|
-
{ attribute: 'rating', comparator: 'greater_than', value: 4 },
|
|
1664
|
-
{ attribute: 'featured', value: true },
|
|
1665
|
-
],
|
|
1666
|
-
},
|
|
1667
|
-
],
|
|
1668
|
-
})) { ... }
|
|
1669
|
-
```
|
|
1670
|
-
|
|
1671
|
-
**Relationship traversal:**
|
|
1684
|
+
##### Nested conditions query
|
|
1672
1685
|
|
|
1673
1686
|
```javascript
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1687
|
+
Product.search({
|
|
1688
|
+
conditions: [
|
|
1689
|
+
{ attribute: 'price', comparator: 'less_than', value: 100 },
|
|
1690
|
+
{
|
|
1691
|
+
operator: 'or',
|
|
1692
|
+
conditions: [
|
|
1693
|
+
{ attribute: 'rating', comparator: 'greater_than', value: 4 },
|
|
1694
|
+
{ attribute: 'featured', value: true },
|
|
1695
|
+
],
|
|
1696
|
+
},
|
|
1697
|
+
],
|
|
1698
|
+
});
|
|
1677
1699
|
```
|
|
1678
1700
|
|
|
1679
|
-
|
|
1701
|
+
##### Chained attribute reference (relationship join)
|
|
1680
1702
|
|
|
1681
1703
|
```javascript
|
|
1682
|
-
|
|
1683
|
-
conditions: [{ attribute: 'inStock', value: true }],
|
|
1684
|
-
sort: { attribute: 'price', descending: false },
|
|
1685
|
-
limit: 10,
|
|
1686
|
-
offset: 20,
|
|
1687
|
-
})) { ... }
|
|
1704
|
+
Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
|
|
1688
1705
|
```
|
|
1689
1706
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
When a field is defined as a relationship (via `@relationship` — see [Defining Relationships](defining-relationships.md)), `select` can pull the related record(s) into your results as nested properties. This is the programmatic equivalent of the REST `select(name,author{name})` syntax (see [Querying REST APIs](querying-rest-apis.md)).
|
|
1693
|
-
|
|
1694
|
-
**Whole related record** — list the relationship field by name. The related record (or an array of records for a to-many relationship) is attached as a nested property:
|
|
1707
|
+
##### Deep nested `select`
|
|
1695
1708
|
|
|
1696
1709
|
```javascript
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
}
|
|
1710
|
+
select: [
|
|
1711
|
+
'id',
|
|
1712
|
+
'name',
|
|
1713
|
+
{ name: 'segments', select: ['id', 'name', { name: 'client', select: ['id', 'name'] }] },
|
|
1714
|
+
];
|
|
1703
1715
|
```
|
|
1704
1716
|
|
|
1705
|
-
|
|
1717
|
+
##### SSR usage
|
|
1706
1718
|
|
|
1707
|
-
```
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1719
|
+
```typescript
|
|
1720
|
+
import { tables } from 'harper';
|
|
1721
|
+
|
|
1722
|
+
export async function render(url: string): Promise<string> {
|
|
1723
|
+
const product = await tables.Product.get(idFromUrl(url));
|
|
1724
|
+
return renderToString(/* <App product={product} /> */);
|
|
1713
1725
|
}
|
|
1714
1726
|
```
|
|
1715
1727
|
|
|
1716
|
-
|
|
1728
|
+
#### Notes
|
|
1717
1729
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1730
|
+
##### `conditions` comparator values
|
|
1731
|
+
|
|
1732
|
+
| Comparator | Description |
|
|
1733
|
+
| -------------------- | ---------------------- |
|
|
1734
|
+
| `equals` | Default equality match |
|
|
1735
|
+
| `greater_than` | Strictly greater |
|
|
1736
|
+
| `greater_than_equal` | Greater than or equal |
|
|
1737
|
+
| `less_than` | Strictly less |
|
|
1738
|
+
| `less_than_equal` | Less than or equal |
|
|
1739
|
+
| `starts_with` | String prefix match |
|
|
1740
|
+
| `contains` | String contains |
|
|
1741
|
+
| `ends_with` | String suffix match |
|
|
1742
|
+
| `between` | Range match |
|
|
1743
|
+
| `not_equal` | Inequality match |
|
|
1744
|
+
|
|
1745
|
+
##### Query object options
|
|
1728
1746
|
|
|
1729
|
-
|
|
1747
|
+
| Property | Description |
|
|
1748
|
+
| ----------------------- | ----------------------------------------------------------------------- |
|
|
1749
|
+
| `conditions` | Array of condition objects to filter records |
|
|
1750
|
+
| `operator` | Top-level `and` (default) or `or` for the `conditions` array |
|
|
1751
|
+
| `limit` | Maximum number of records to return |
|
|
1752
|
+
| `offset` | Number of records to skip (for pagination) |
|
|
1753
|
+
| `select` | Properties to include in each returned record |
|
|
1754
|
+
| `sort` | Sort order object with `attribute`, `descending`, and `next` properties |
|
|
1755
|
+
| `explain` | If `true`, returns conditions reordered as Harper will execute them |
|
|
1756
|
+
| `enforceExecutionOrder` | If `true`, forces conditions to execute in the order supplied |
|
|
1730
1757
|
|
|
1731
|
-
|
|
1732
|
-
- Selecting a relationship without filtering on it produces LEFT JOIN behavior (records with no related row are still returned); adding a condition on a related attribute (e.g. `attribute: ['author', 'name']`) produces INNER JOIN behavior.
|
|
1758
|
+
##### `select` special properties
|
|
1733
1759
|
|
|
1734
|
-
|
|
1760
|
+
- `$id` — Returns the primary key regardless of its name
|
|
1761
|
+
- `$updatedtime` — Returns the last-updated timestamp
|
|
1762
|
+
- `$distance` — Returns the computed distance from the target vector when the query uses a vector index
|
|
1735
1763
|
|
|
1736
|
-
|
|
1764
|
+
##### Relationship join behavior
|
|
1765
|
+
|
|
1766
|
+
- Selecting a relationship **without** filtering on it behaves as a **LEFT JOIN** — records with no related row are still returned.
|
|
1767
|
+
- Adding a `conditions` entry on a related attribute (e.g. `attribute: ['author', 'name']`) behaves as an **INNER JOIN** — only records with a matching related row are returned.
|
|
1768
|
+
|
|
1769
|
+
- Keep `harper` external when bundling for SSR (e.g. `ssr: { external: ['harper'] }` in `vite.config`) so it resolves to the runtime instead of being bundled.
|
|
1770
|
+
- `tables`, `databases`, and other Harper APIs are the same live, process-wide objects regardless of whether accessed as globals or via `import { tables } from 'harper'`.
|
|
1737
1771
|
|
|
1738
1772
|
### 3.4 TypeScript Type Stripping in Harper
|
|
1739
1773
|
|
|
@@ -2194,6 +2228,34 @@ static:
|
|
|
2194
2228
|
- Install dependencies: `npm install --save-dev vite @harperfast/vite @vitejs/plugin-react` (swap in your framework's Vite plugin, e.g. `@vitejs/plugin-vue`).
|
|
2195
2229
|
- Then `harper dev .` runs the app with HMR and `harper run .` runs the production build. Vite does _not_ need to be executed separately.
|
|
2196
2230
|
|
|
2231
|
+
#### Reading Harper Data During SSR
|
|
2232
|
+
|
|
2233
|
+
The render entry (`src/entry-server.tsx`) runs **inside Harper**, so it can read straight from the database and render the data into the HTML — no client-side fetch/XHR. `tables` is the same live, process-wide registry available everywhere (see [Programmatic Table Requests](programmatic-table-requests.md)); import it and query a table in an async `render`:
|
|
2234
|
+
|
|
2235
|
+
```tsx
|
|
2236
|
+
import { tables } from 'harper';
|
|
2237
|
+
|
|
2238
|
+
export async function render(url: string): Promise<string> {
|
|
2239
|
+
const product = await tables.Product.get(idFromUrl(url));
|
|
2240
|
+
return renderToString(
|
|
2241
|
+
<StrictMode>
|
|
2242
|
+
<App product={product} />
|
|
2243
|
+
</StrictMode>,
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
```
|
|
2247
|
+
|
|
2248
|
+
Keep `harper` external in `vite.config.ts` so this import resolves to Harper's running runtime instead of being bundled. `node_modules/harper` is symlinked to the running install, and symlinked deps aren't reliably auto-externalized for SSR:
|
|
2249
|
+
|
|
2250
|
+
```typescript
|
|
2251
|
+
export default defineConfig({
|
|
2252
|
+
ssr: { external: ['harper'] },
|
|
2253
|
+
// ...plugins, resolve, build
|
|
2254
|
+
});
|
|
2255
|
+
```
|
|
2256
|
+
|
|
2257
|
+
To hydrate on the client without re-fetching, embed the rendered data in the HTML (e.g. an inline `<script type="application/json">`) and read it back before hydration — so the page needs no XHR at all.
|
|
2258
|
+
|
|
2197
2259
|
#### Deploying to Production
|
|
2198
2260
|
|
|
2199
2261
|
Because `@harperfast/vite` builds on the node and `static` serves the output, deploy the component as-is — no manual build-and-move step is needed:
|