@harperfast/template-vue-ts-studio 1.9.2 → 1.10.1

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.
Files changed (26) hide show
  1. package/.agents/skills/harper-best-practices/AGENTS.md +916 -338
  2. package/.agents/skills/harper-best-practices/rules/caching.md +68 -62
  3. package/.agents/skills/harper-best-practices/rules/custom-resources.md +144 -23
  4. package/.agents/skills/harper-best-practices/rules/defining-relationships.md +152 -22
  5. package/.agents/skills/harper-best-practices/rules/extending-tables.md +90 -21
  6. package/.agents/skills/harper-best-practices/rules/handling-binary-data.md +103 -23
  7. package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +143 -91
  8. package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
  9. package/.agents/skills/harper-best-practices/rules/serving-web-content.md +28 -0
  10. package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +97 -16
  11. package/.agents/skills/harper-best-practices/rules/vector-indexing.md +59 -29
  12. package/.agents/skills/harper-best-practices/rules.manifest.yaml +109 -7
  13. package/agent/skills/harper-best-practices/AGENTS.md +916 -338
  14. package/agent/skills/harper-best-practices/rules/caching.md +68 -62
  15. package/agent/skills/harper-best-practices/rules/custom-resources.md +144 -23
  16. package/agent/skills/harper-best-practices/rules/defining-relationships.md +152 -22
  17. package/agent/skills/harper-best-practices/rules/extending-tables.md +90 -21
  18. package/agent/skills/harper-best-practices/rules/handling-binary-data.md +103 -23
  19. package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +143 -91
  20. package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
  21. package/agent/skills/harper-best-practices/rules/serving-web-content.md +28 -0
  22. package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +97 -16
  23. package/agent/skills/harper-best-practices/rules/vector-indexing.md +59 -29
  24. package/agent/skills/harper-best-practices/rules.manifest.yaml +109 -7
  25. package/package.json +1 -1
  26. package/skills-lock.json +1 -1
@@ -2,45 +2,114 @@
2
2
  name: extending-tables
3
3
  description: How to add custom logic to automatically generated table resources in Harper.
4
4
  metadata:
5
- mode: synthesized
5
+ mode: generate
6
+ sources:
7
+ - reference/v5/resources/overview.md#Extending a Table
8
+ - reference/v5/resources/resource-api.md#Throwing Errors
9
+ sourceCommit: ce0ab713d918d789bc1c9f22e461e963ccc1dff1
10
+ inputHash: 19738fbc732e0a1a
6
11
  ---
7
12
 
8
13
  # Extending Tables
9
14
 
10
- Instructions for the agent to follow when extending table resources in Harper.
15
+ Instructions for the agent to follow when adding custom logic to automatically generated table resources in Harper.
11
16
 
12
17
  ## When to Use
13
18
 
14
- Use this skill when you need to add custom validation, side effects (like webhooks), data transformation, or custom access control to the standard CRUD operations of a Harper table.
19
+ Apply this rule when you need to add computed properties, intercept writes, enforce validation, or otherwise customize the behavior of a Harper table resource beyond what the default generated endpoints provide. Use it any time a `@table` type needs server-side logic attached to its REST handlers.
15
20
 
16
21
  ## How It Works
17
22
 
18
- 1. **Define the Table in GraphQL**: In your `.graphql` schema, define the table using the `@table` directive. **Do not** use `@export` if you plan to extend it.
23
+ 1. **Define the schema without `@export`**: Declare the table type in `schema.graphql` and omit the `@export` directive. Leaving `@export` on the schema while also exporting a subclass with the same name produces conflicting endpoints. Let the JavaScript class own the URL instead.
24
+
19
25
  ```graphql
26
+ # Omit the `@export` directive
20
27
  type MyTable @table {
21
- id: ID @primaryKey
22
- name: String
28
+ id: Long @primaryKey
29
+ # ...
23
30
  }
24
31
  ```
25
- 2. **Create the Extension File**: Create a `.ts` file in your `resources/` directory.
26
- 3. **Extend the Table Resource**: Export a class that extends `tables.YourTableName` and override the relevant **static** methods. In Harper 5 resource handlers are static and map 1:1 to HTTP verbs: `get(target)`, `post(target, data)`, `put(target, data)`, `patch(target, data)`, `delete(target)`. `target` is a pre-parsed `RequestTarget`; for writes, `data` is the request body and is **awaitable** (`await data`). Delegate to `super` to keep Harper's default behavior — a collection create passes just the record (`super.post(record)`), updates pass the target (`super.put(target, data)` / `super.patch(target, data)`), and reads/deletes pass the target (`super.get(target)`). To return a specific HTTP status from a thrown error, set **`.statusCode`** (e.g. `400`) on the error — a plain `.status` property is ignored.
27
32
 
28
- ```typescript
29
- import { tables } from 'harper';
33
+ 2. **Extend the generated table class**: In `resources.js`, extend from the `tables.<TypeName>` global. The class name you export becomes the URL path. The exported class extends tables.
30
34
 
35
+ ```javascript
31
36
  export class MyTable extends tables.MyTable {
32
- // Static handler; receives (target, data) — data is awaitable.
33
- static async post(target: any, data: any) {
34
- const record = await data;
35
- if (!record?.name) {
36
- const error: any = new Error('Name is required');
37
- error.statusCode = 400; // HTTP status (use statusCode, NOT status)
38
- throw error;
39
- }
40
- return super.post(record); // create delegates with the record (no id)
37
+ static async get(target) {
38
+ const record = await super.get(target);
39
+ return { ...record, computedField: 'value' };
40
+ }
41
+
42
+ static async post(target, data) {
43
+ this.create({ ...(await data), status: 'pending' });
41
44
  }
42
45
  }
43
46
  ```
44
47
 
45
- 4. **Override Methods**: Override the static `get`, `post`, `put`, `patch`, or `delete` as needed, delegating to `super.<method>` (see the argument forms above) to preserve Harper's default behavior unless you intend to replace it entirely.
46
- 5. **Implement Logic**: Use overrides for validation, side effects, or transforming data before/after database operations.
48
+ 3. **Call `super` to preserve default behavior**: When delegating to `super`, match the argument form to the operation:
49
+ - Reads/deletes: `super.get(target)` / `super.delete(target)`
50
+ - Collection create: `super.post(target, record)` — target carries no id
51
+ - Updates: `super.put(target, data)` / `super.patch(target, data)`
52
+
53
+ Omit the `super` call only if you intend to replace the default behavior entirely.
54
+
55
+ 4. **Set `statusCode` on thrown errors to control HTTP responses**: Uncaught errors are caught by the protocol handler and produce error responses for REST. Use `.statusCode` — a plain `.status` property is ignored.
56
+
57
+ ```javascript
58
+ const error = new Error('Name is required');
59
+ error.statusCode = 400; // use statusCode, NOT status
60
+ throw error;
61
+ ```
62
+
63
+ 5. **Configure Harper to load both files**: Ensure your configuration references the schema and resource files.
64
+
65
+ ```yaml
66
+ rest: true
67
+ graphqlSchema:
68
+ files: schema.graphql
69
+ jsResource:
70
+ files: resources.js
71
+ ```
72
+
73
+ ## Examples
74
+
75
+ Full end-to-end example — schema, resource class, and error handling:
76
+
77
+ ```graphql
78
+ # schema.graphql — omit @export so the JS class owns the endpoint
79
+ type MyTable @table {
80
+ id: Long @primaryKey
81
+ }
82
+ ```
83
+
84
+ ```javascript
85
+ // resources.js
86
+ export class MyTable extends tables.MyTable {
87
+ static async get(target) {
88
+ // get the record from the database
89
+ const record = await super.get(target);
90
+ // add a computed property before returning
91
+ return { ...record, computedField: 'value' };
92
+ }
93
+
94
+ static async post(target, data) {
95
+ // custom action on POST
96
+ this.create({ ...(await data), status: 'pending' });
97
+ }
98
+ }
99
+ ```
100
+
101
+ Throwing a controlled HTTP error:
102
+
103
+ ```javascript
104
+ if (!authorized) {
105
+ const error = new Error('Forbidden');
106
+ error.statusCode = 403;
107
+ throw error;
108
+ }
109
+ ```
110
+
111
+ ## Notes
112
+
113
+ - Always omit `@export` from the schema type when a JavaScript subclass is exporting the same name. The two registrations conflict.
114
+ - `super` must be called with the correct arguments for each operation type — mismatched arguments will not behave as expected.
115
+ - `statusCode` is the only recognized property for controlling HTTP status on thrown errors; `.status` is ignored.
@@ -2,44 +2,124 @@
2
2
  name: handling-binary-data
3
3
  description: How to store and serve binary data like images or audio in Harper.
4
4
  metadata:
5
- mode: synthesized
5
+ mode: generate
6
+ sources:
7
+ - reference/v5/database/api.md#Accepting Binary in JSON Requests
8
+ - reference/v5/database/api.md#Serving Binary from a Resource
9
+ - reference/v5/rest/content-types.md#Storing Arbitrary Content Types
10
+ sourceCommit: ce0ab713d918d789bc1c9f22e461e963ccc1dff1
11
+ inputHash: fa06480e6fae7614
6
12
  ---
7
13
 
8
14
  # Handling Binary Data
9
15
 
10
- Instructions for the agent to follow when handling binary data in Harper.
16
+ Instructions for the agent to follow when storing and serving binary data (images, audio, arbitrary content types) in Harper.
11
17
 
12
18
  ## When to Use
13
19
 
14
- Use this skill when you need to store binary files (images, audio, etc.) in the database or serve them back to clients via REST endpoints.
20
+ Apply this rule when a Harper resource needs to accept, store, or serve binary payloads such as images, audio files, or calendar data. Use it when REST clients send `base64`-encoded data inside JSON, when raw binary is uploaded via `PUT`/`POST`, or when a resource must stream binary back to the client with the correct `Content-Type`.
15
21
 
16
22
  ## How It Works
17
23
 
18
- 1. **Store Binary Data**: In your resource's `post` or `put` method, convert incoming data to Buffers and then to Blobs using `createBlob` from Harper's globals. Include the MIME type if available:
24
+ 1. **Accept base64-encoded binary from JSON clients**: Decode the incoming `base64` string with `Buffer.from` and wrap it using `createBlob`, recording the MIME type. Override `post` in your resource class:
19
25
 
20
26
  ```typescript
21
- async post(target, record) {
22
- if (record.data) {
23
- record.data = createBlob(Buffer.from(record.data, record.encoding || 'base64'), {
24
- type: record.contentType || 'application/octet-stream',
25
- });
26
- }
27
- return super.post(target, record);
27
+ import { type RequestTargetOrId, tables, createBlob } from 'harper';
28
+
29
+ export class Photo extends tables.Photo {
30
+ static async post(target: RequestTargetOrId, record: any) {
31
+ if (record.data) {
32
+ record.data = createBlob(Buffer.from(record.data, record.encoding || 'base64'), {
33
+ type: record.contentType || 'application/octet-stream',
34
+ });
35
+ }
36
+ return super.post(target, record);
37
+ }
28
38
  }
29
39
  ```
30
40
 
31
- 2. **Serve Binary Data**: In your resource's `get` method, return a response object with the appropriate `Content-Type` and the binary data in the `body`:
41
+ 2. **Serve binary from a resource**: Override `get` to return a response object with the blob's MIME type in the `Content-Type` header and the blob as the body. Harper streams it to the client:
42
+
32
43
  ```typescript
33
- async get(target) {
34
- const record = await super.get(target);
35
- if (record?.data) {
36
- return {
37
- status: 200,
38
- headers: { 'Content-Type': record.data.type || 'application/octet-stream' },
39
- body: record.data,
40
- };
41
- }
42
- return record;
44
+ export class Photo extends tables.Photo {
45
+ static async get(target: RequestTargetOrId) {
46
+ const record = await super.get(target);
47
+ if (record?.data) {
48
+ return {
49
+ status: 200,
50
+ headers: { 'Content-Type': record.data.type || 'application/octet-stream' },
51
+ body: record.data,
52
+ };
53
+ }
54
+ return record;
55
+ }
43
56
  }
44
57
  ```
45
- 3. **Use the Blob Type**: Ensure your GraphQL schema uses the `Blob` scalar for binary fields.
58
+
59
+ 3. **Upload raw binary with a non-standard content type**: Make a `PUT` or `POST` with any non-standard `Content-Type` header. Harper automatically stores the body as a record with `contentType` and `data` properties:
60
+
61
+ ```http
62
+ PUT /my-resource/33
63
+ Content-Type: text/calendar
64
+
65
+ BEGIN:VCALENDAR
66
+ VERSION:2.0
67
+ ...
68
+ ```
69
+
70
+ Harper stores this as:
71
+
72
+ ```json
73
+ { "contentType": "text/calendar", "data": "BEGIN:VCALENDAR\nVERSION:2.0\n..." }
74
+ ```
75
+
76
+ Retrieving that record returns the response with the stored `Content-Type` and body. If the content type is not from the `text` family, the data is treated as binary (a Node.js `Buffer`).
77
+
78
+ 4. **Upload binary to a specific property**: Use `application/octet-stream` (or any image/binary MIME type) and target a sub-path to store binary directly on a property:
79
+
80
+ ```http
81
+ PUT /my-resource/33/image
82
+ Content-Type: image/gif
83
+
84
+ ...image data...
85
+ ```
86
+
87
+ ## Examples
88
+
89
+ **End-to-end: accept base64 JSON, store as blob, serve as binary**
90
+
91
+ ```typescript
92
+ import { type RequestTargetOrId, tables, createBlob } from 'harper';
93
+
94
+ export class Photo extends tables.Photo {
95
+ // Accept base64-encoded uploads in JSON
96
+ static async post(target: RequestTargetOrId, record: any) {
97
+ if (record.data) {
98
+ record.data = createBlob(Buffer.from(record.data, record.encoding || 'base64'), {
99
+ type: record.contentType || 'application/octet-stream',
100
+ });
101
+ }
102
+ return super.post(target, record);
103
+ }
104
+
105
+ // Stream the blob back with the correct Content-Type
106
+ static async get(target: RequestTargetOrId) {
107
+ const record = await super.get(target);
108
+ if (record?.data) {
109
+ return {
110
+ status: 200,
111
+ headers: { 'Content-Type': record.data.type || 'application/octet-stream' },
112
+ body: record.data,
113
+ };
114
+ }
115
+ return record;
116
+ }
117
+ }
118
+ ```
119
+
120
+ ## Notes
121
+
122
+ - `createBlob` takes a `Buffer` as its first argument and an options object with a `type` property for the MIME type. See [using-blob-datatype.md](using-blob-datatype.md) for full details on the blob data type.
123
+ - Always fall back to `application/octet-stream` when no MIME type is known, both when creating and when serving blobs.
124
+ - When Harper retrieves a record that has both `contentType` and `data` properties, it automatically sets the response `Content-Type` and body — no custom `get` override is required for that case unless you need additional logic.
125
+ - Non-`text` content types cause `data` to be stored and returned as a Node.js `Buffer`.
@@ -2,133 +2,185 @@
2
2
  name: programmatic-table-requests
3
3
  description: How to interact with Harper tables programmatically using the `tables` object.
4
4
  metadata:
5
- mode: synthesized
5
+ mode: generate
6
+ sources:
7
+ - reference/v5/database/api.md#`tables`
8
+ - reference/v5/resources/resource-api.md#Query Object
9
+ - 'reference/v5/database/api.md#`transaction(context?, callback)`'
10
+ - >-
11
+ reference/v5/resources/resource-api.md#`update(target: RequestTarget | Id,
12
+ updates?: object): Promise<Resource>`
13
+ - >-
14
+ reference/v5/resources/resource-api.md#`addTo(property: string, value:
15
+ number)`
16
+ - reference/v5/components/javascript-environment.md#Module Loading
17
+ sourceCommit: be709f9978319dcdb669c05d794effc82bcda8b7
18
+ inputHash: 3d2417fbff687c42
6
19
  ---
7
20
 
8
21
  # Programmatic Table Requests
9
22
 
10
- Instructions for the agent to follow when interacting with Harper tables via code.
23
+ Instructions for the agent to interact with Harper tables programmatically using the `tables` object and its query API.
11
24
 
12
25
  ## When to Use
13
26
 
14
- Use this skill when you need to perform database operations (CRUD, search, subscribe) from within Harper Resources or scripts.
27
+ 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`.
15
28
 
16
29
  ## How It Works
17
30
 
18
- 1. **Access the Table**: Use the global `tables` object followed by your table name (e.g., `tables.MyTable`).
19
- 2. **Perform CRUD Operations**:
20
- - **Get**: `await tables.MyTable.get(id)` for a single record or `await tables.MyTable.get({ conditions: [...] })` for multiple.
21
- - **Create**: `await tables.MyTable.post(record)` (auto-generates ID) or `await tables.MyTable.put(id, record)`.
22
- - **Update**: `await tables.MyTable.patch(id, partialRecord)` for partial updates.
23
- - **Delete**: `await tables.MyTable.delete(id)`.
24
- 3. **Use Updatable Records for Atomic Ops**: Call `update(id)` to get a reference, then use `addTo` or `subtractFrom` for atomic increments/decrements:
25
- ```typescript
26
- const stats = await tables.Stats.update('daily');
27
- stats.addTo('viewCount', 1);
28
- ```
29
- 4. **Search and Stream**: Use `search(query)` for efficient streaming of large result sets:
30
- ```typescript
31
- for await (const record of tables.MyTable.search({ conditions: [...] })) {
32
- // process record
33
- }
31
+ 1. **Import `tables`**: Import from the `harper` package. Each table defined in `schema.graphql` with `@table` is available as a named property.
32
+
33
+ ```javascript
34
+ import { tables } from 'harper';
35
+ const { Product } = tables;
36
+ // same as: databases.data.Product
34
37
  ```
35
- See the [Query Conditions](#query-conditions) section below for the full query object reference.
36
- 5. **Real-time Subscriptions**: Use `subscribe(query)` to listen for changes:
37
- ```typescript
38
- for await (const event of tables.MyTable.subscribe(query)) {
39
- // handle event
38
+
39
+ 2. **Define your schema**: Declare tables in `schema.graphql` using `@table` and `@primaryKey`.
40
+
41
+ ```graphql
42
+ # schema.graphql
43
+ type Product @table {
44
+ id: Long @primaryKey
45
+ name: String
46
+ price: Float
40
47
  }
41
48
  ```
42
- 6. **Publish Events**: Use `publish(id, message)` to trigger subscriptions without necessarily persisting data.
43
49
 
44
- ## Query Conditions
50
+ 3. **Create and mutate records**: Use `create`, `patch`, `get`, and `update` on the table class.
45
51
 
46
- When passing a query to `search()`, `get()`, or `subscribe()`, use a query object with a `conditions` array.
52
+ ```javascript
53
+ // Create a new record (id auto-generated)
54
+ const created = await Product.create({ name: 'Shirt', price: 9.5 });
47
55
 
48
- ### Condition Object Shape
56
+ // Modify the record
57
+ await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
49
58
 
50
- | Property | Description |
51
- | ------------ | ------------------------------------------------------------------------------------------ |
52
- | `attribute` | Field name, or array of field names to traverse a relationship (e.g., `['brand', 'name']`) |
53
- | `value` | The value to compare against |
54
- | `comparator` | One of the comparator strings below (default: `equals`) |
55
- | `operator` | `and` (default) or `or` — applies to a nested `conditions` block |
56
- | `conditions` | Nested array of condition objects for complex AND/OR logic |
59
+ // Retrieve by primary key
60
+ const record = await Product.get(created.id);
61
+ ```
57
62
 
58
- ### Comparator Values
63
+ 4. **Query with `search(` and `conditions`**: Pass a query object to `search()` to filter records. Iterate the async result.
59
64
 
60
- Use these exact strings — incorrect comparator names will silently fail or error:
65
+ ```javascript
66
+ const query = {
67
+ conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
68
+ };
69
+ for await (const record of Product.search(query)) {
70
+ // ...
71
+ }
72
+ ```
61
73
 
62
- | Comparator | Meaning |
63
- | -------------------- | ---------------------------------------------------------- |
64
- | `equals` | Exact match (default) |
65
- | `not_equal` | Not equal |
66
- | `greater_than` | `>` |
67
- | `greater_than_equal` | `>=` |
68
- | `less_than` | `<` |
69
- | `less_than_equal` | `<=` |
70
- | `starts_with` | String starts with value |
71
- | `contains` | String contains value |
72
- | `ends_with` | String ends with value |
73
- | `between` | Value is between two bounds (pass `value` as `[min, max]`) |
74
+ 5. **Use `select` to shape results**: Pass a `select` array to return only specific properties, including nested relationship fields.
74
75
 
75
- ### Query Object Parameters
76
+ ```javascript
77
+ const book = await Book.get({ id: 42, select: ['id', 'title', 'author'] });
78
+ book.author.name; // full related Author record
76
79
 
77
- | Property | Description |
78
- | ------------ | ------------------------------------------------------------------------------------ |
79
- | `conditions` | Array of condition objects |
80
- | `limit` | Maximum number of records to return |
81
- | `offset` | Number of records to skip (for pagination) |
82
- | `select` | Array of attribute names to return; supports `$id` and `$updatedtime` |
83
- | `sort` | Object with `attribute`, `descending` (bool), and optional `next` for secondary sort |
80
+ // Partial related record
81
+ const book = await Book.get({
82
+ id: 42,
83
+ select: ['id', 'title', { name: 'author', select: ['name'] }],
84
+ });
85
+ ```
84
86
 
85
- ### Examples
87
+ 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.
86
88
 
87
- **Simple filter:**
89
+ ```javascript
90
+ static async post(target, data) {
91
+ const record = await this.update(target.id);
92
+ record.addTo('quantity', -1); // decrement safely across nodes
93
+ }
94
+ ```
88
95
 
89
- ```javascript
90
- for await (const record of tables.Product.search({
91
- conditions: [{ attribute: 'price', comparator: 'less_than', value: 100 }],
92
- limit: 20,
93
- })) { ... }
94
- ```
96
+ 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.
95
97
 
96
- **AND + nested OR:**
98
+ ## Examples
99
+
100
+ ### Nested conditions query
97
101
 
98
102
  ```javascript
99
- for await (const record of tables.Product.search({
100
- conditions: [
101
- { attribute: 'price', comparator: 'less_than', value: 100 },
102
- {
103
- operator: 'or',
104
- conditions: [
105
- { attribute: 'rating', comparator: 'greater_than', value: 4 },
106
- { attribute: 'featured', value: true },
107
- ],
108
- },
109
- ],
110
- })) { ... }
103
+ Product.search({
104
+ conditions: [
105
+ { attribute: 'price', comparator: 'less_than', value: 100 },
106
+ {
107
+ operator: 'or',
108
+ conditions: [
109
+ { attribute: 'rating', comparator: 'greater_than', value: 4 },
110
+ { attribute: 'featured', value: true },
111
+ ],
112
+ },
113
+ ],
114
+ });
111
115
  ```
112
116
 
113
- **Relationship traversal:**
117
+ ### Chained attribute reference (relationship join)
114
118
 
115
119
  ```javascript
116
- for await (const record of tables.Book.search({
117
- conditions: [{ attribute: ['brand', 'name'], comparator: 'equals', value: 'Harper' }],
118
- })) { ... }
120
+ Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
119
121
  ```
120
122
 
121
- **Sort and paginate:**
123
+ ### Deep nested `select`
122
124
 
123
125
  ```javascript
124
- for await (const record of tables.Product.search({
125
- conditions: [{ attribute: 'inStock', value: true }],
126
- sort: { attribute: 'price', descending: false },
127
- limit: 10,
128
- offset: 20,
129
- })) { ... }
126
+ select: [
127
+ 'id',
128
+ 'name',
129
+ { name: 'segments', select: ['id', 'name', { name: 'client', select: ['id', 'name'] }] },
130
+ ];
130
131
  ```
131
132
 
132
- ## Cautions
133
+ ### SSR usage
134
+
135
+ ```typescript
136
+ import { tables } from 'harper';
137
+
138
+ export async function render(url: string): Promise<string> {
139
+ const product = await tables.Product.get(idFromUrl(url));
140
+ return renderToString(/* <App product={product} /> */);
141
+ }
142
+ ```
133
143
 
134
- 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.
144
+ ## Notes
145
+
146
+ ### `conditions` comparator values
147
+
148
+ | Comparator | Description |
149
+ | -------------------- | ---------------------- |
150
+ | `equals` | Default equality match |
151
+ | `greater_than` | Strictly greater |
152
+ | `greater_than_equal` | Greater than or equal |
153
+ | `less_than` | Strictly less |
154
+ | `less_than_equal` | Less than or equal |
155
+ | `starts_with` | String prefix match |
156
+ | `contains` | String contains |
157
+ | `ends_with` | String suffix match |
158
+ | `between` | Range match |
159
+ | `not_equal` | Inequality match |
160
+
161
+ ### Query object options
162
+
163
+ | Property | Description |
164
+ | ----------------------- | ----------------------------------------------------------------------- |
165
+ | `conditions` | Array of condition objects to filter records |
166
+ | `operator` | Top-level `and` (default) or `or` for the `conditions` array |
167
+ | `limit` | Maximum number of records to return |
168
+ | `offset` | Number of records to skip (for pagination) |
169
+ | `select` | Properties to include in each returned record |
170
+ | `sort` | Sort order object with `attribute`, `descending`, and `next` properties |
171
+ | `explain` | If `true`, returns conditions reordered as Harper will execute them |
172
+ | `enforceExecutionOrder` | If `true`, forces conditions to execute in the order supplied |
173
+
174
+ ### `select` special properties
175
+
176
+ - `$id` — Returns the primary key regardless of its name
177
+ - `$updatedtime` — Returns the last-updated timestamp
178
+ - `$distance` — Returns the computed distance from the target vector when the query uses a vector index
179
+
180
+ ### Relationship join behavior
181
+
182
+ - Selecting a relationship **without** filtering on it behaves as a **LEFT JOIN** — records with no related row are still returned.
183
+ - 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.
184
+
185
+ - 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.
186
+ - `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'`.