@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.
@@ -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 Harper's `Blob` data type.
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 you need streaming support or want to avoid loading the entire value into memory. See [handling-binary-data.md](handling-binary-data.md) for broader binary data guidance.
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 a `@table` type.
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, string, or stream as the first argument. Pass a `BlobOptions` object as the second argument to configure behavior.
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. **Read blob data using standard Web API methods**: The `Blob` type implements the Web API `Blob` interface. Use `.bytes()`, `.text()`, `.arrayBuffer()`, `.stream()`, or `.slice()` to access content.
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` for ACID-compliant writes**: By default, blobs are not ACID-compliant — a record can reference a blob before it is fully written. Set `saveBeforeCommit: true` to wait for the full write before the transaction commits.
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 to avoid stale records.
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`. Manual `createBlob()` calls are not required for plain JSON HTTP bodies or MQTT messages in most cases.
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` Reference
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 for the blob to be fully written before committing the transaction. |
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 large media with low latency:**
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
- **Guaranteed write before commit:**
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; `Bytes` does not. Prefer `Blob` for content larger than 20KB.
612
- - All standard Web API `Blob` methods are available: `.bytes()`, `.text()`, `.arrayBuffer()`, `.stream()`, `.slice()`.
613
- - Blobs are **not** ACID-compliant by default when created from a stream. Use `saveBeforeCommit: true` to enforce transactional consistency.
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 programmatic routes in a Harper application. Use it any time business logic needs to live outside a standard table-backed resource.
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**: Always import explicitly rather than relying on globals.
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`**: Use `export class` so Harper can expose it as an endpoint. Implement HTTP methods as `static` methods on the class.
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. **Add async `static` methods for each HTTP verb you need**: Methods receive `target` (contains `target.id`, etc.) and, for write operations, `data`.
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. **Control the URL by choosing the export form**: The shape of the export determines the resulting URL path. Path matching is case-sensitive.
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
- | Export form | URL | Notes |
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
- 5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the class.
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
- 6. **Optionally use the resource as a cache source for a local table**: Pass the class to `sourcedFrom`.
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
- Wrap an external API and expose it as an endpoint, then back a local cache table with it:
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
- Programmatic registration with a custom path:
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 CustomEndpoint extends Resource {
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', CustomEndpoint);
1489
+ server.resources.set('my-path', Foo);
1452
1490
  ```
1453
1491
 
1454
1492
  #### Notes
1455
1493
 
1456
- - `export class` directly produces a URL from the class name (e.g., `export class Foo extends Resource {}` `/Foo/`). Do not export the same resource from both a schema file and a JavaScript file — this creates conflicting exports.
1457
- - URL path segments are case-sensitive: `/Foo/` and `/foo/` are different endpoints.
1458
- - For CommonJS modules, use `const { tables, Resource } = require('harper');` instead of the ESM import.
1459
- - When developing a component in its own directory, run `npm link harper` to ensure typings match your installed version. All installed components have `harper` automatically linked.
1460
- - The `static` keyword is required on all HTTP verb methods Harper dispatches requests through static class methods, not instance methods.
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 follow when interacting with Harper tables via code.
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
- Use this skill when you need to perform database operations (CRUD, search, subscribe) from within Harper Resources or scripts.
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. **Access the Table**: Use the global `tables` object followed by your table name (e.g., `tables.MyTable`).
1577
- 2. **Perform CRUD Operations**:
1578
- - **Get**: `await tables.MyTable.get(id)` for a single record or `await tables.MyTable.get({ conditions: [...] })` for multiple.
1579
- - **Create**: `await tables.MyTable.post(record)` (auto-generates ID) or `await tables.MyTable.put(id, record)`.
1580
- - **Update**: `await tables.MyTable.patch(id, partialRecord)` for partial updates.
1581
- - **Delete**: `await tables.MyTable.delete(id)`.
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
- See the [Query Conditions](#query-conditions) section below for the full query object reference.
1594
- 5. **Real-time Subscriptions**: Use `subscribe(query)` to listen for changes:
1595
- ```typescript
1596
- for await (const event of tables.MyTable.subscribe(query)) {
1597
- // handle event
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
- #### Query Conditions
1634
+ 3. **Create and mutate records**: Use `create`, `patch`, `get`, and `update` on the table class.
1603
1635
 
1604
- When passing a query to `search()`, `get()`, or `subscribe()`, use a query object with a `conditions` array.
1636
+ ```javascript
1637
+ // Create a new record (id auto-generated)
1638
+ const created = await Product.create({ name: 'Shirt', price: 9.5 });
1605
1639
 
1606
- ##### Condition Object Shape
1640
+ // Modify the record
1641
+ await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
1607
1642
 
1608
- | Property | Description |
1609
- | ------------ | ------------------------------------------------------------------------------------------ |
1610
- | `attribute` | Field name, or array of field names to traverse a relationship (e.g., `['brand', 'name']`) |
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
- ##### Comparator Values
1647
+ 4. **Query with `search(` and `conditions`**: Pass a query object to `search()` to filter records. Iterate the async result.
1617
1648
 
1618
- Use these exact strings — incorrect comparator names will silently fail or error:
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
- | Comparator | Meaning |
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
- ##### Query Object Parameters
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
- | Property | Description |
1636
- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1637
- | `conditions` | Array of condition objects |
1638
- | `limit` | Maximum number of records to return |
1639
- | `offset` | Number of records to skip (for pagination) |
1640
- | `select` | Array of fields to return: attribute names (supports `$id` and `$updatedtime`), or an object `{ name, select }` to include fields from a related record — see [Selecting Related Data](#selecting-related-data) |
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
- ##### Examples
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
- **Simple filter:**
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
- ```javascript
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
- **AND + nested OR:**
1682
+ #### Examples
1655
1683
 
1656
- ```javascript
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
- for await (const record of tables.Book.search({
1675
- conditions: [{ attribute: ['brand', 'name'], comparator: 'equals', value: 'Harper' }],
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
- **Sort and paginate:**
1701
+ ##### Chained attribute reference (relationship join)
1680
1702
 
1681
1703
  ```javascript
1682
- for await (const record of tables.Product.search({
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
- #### Selecting Related Data
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
- for await (const book of tables.Book.search({
1698
- conditions: [{ attribute: 'id', value: 42 }],
1699
- select: ['id', 'title', 'author'], // `author` is a relationship field
1700
- })) {
1701
- console.log(book.author.name); // the full related Author record
1702
- }
1710
+ select: [
1711
+ 'id',
1712
+ 'name',
1713
+ { name: 'segments', select: ['id', 'name', { name: 'client', select: ['id', 'name'] }] },
1714
+ ];
1703
1715
  ```
1704
1716
 
1705
- **Partial related record** — use an object `{ name, select }` to choose which fields of the related record to return. Unselected fields are omitted:
1717
+ ##### SSR usage
1706
1718
 
1707
- ```javascript
1708
- for await (const book of tables.Book.search({
1709
- conditions: [{ attribute: ['author', 'name'], comparator: 'equals', value: 'Harper' }],
1710
- select: ['id', 'title', { name: 'author', select: ['name'] }],
1711
- })) {
1712
- // book.author.name is present; other Author fields are undefined
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
- **Nesting** — a `select` inside an object entry may itself contain more `{ name, select }` objects, traversing multiple relationships in one query:
1728
+ #### Notes
1717
1729
 
1718
- ```javascript
1719
- select: [
1720
- 'id',
1721
- 'name',
1722
- {
1723
- name: 'segments',
1724
- select: ['id', 'name', { name: 'client', select: ['id', 'name'] }],
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
- Notes:
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
- - A to-many relationship resolves to an array of records; depending on access pattern you may need to `await` the property before iterating it.
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
- #### Cautions
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
- Be very careful when performing updates and deletions! You may be dealing with live production data. The wrong request to delete, without approval from a human, could be devastating to a business. Always use the proper approval process.
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: