@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
@@ -5,21 +5,21 @@ metadata:
5
5
  mode: generate
6
6
  sources:
7
7
  - learn/developers/caching-with-harper.md
8
- sourceCommit: b7fbddadd42eb4487190b650a9abc4bcfeef5819
9
- inputHash: 8212a7d7dbbd2b18
8
+ sourceCommit: 4fe4c9c95e0974eaa77032f6f10e36fbd8ec64ac
9
+ inputHash: 60ad55fa37b5eec5
10
10
  ---
11
11
 
12
12
  # Caching External Data Sources in Harper
13
13
 
14
- Instructions for the agent to implement integrated data caching in Harper by wrapping external sources with a cache table and `sourcedFrom`.
14
+ Instructions for the agent to implement integrated data caching from external sources using Harper's cache table directives and `sourcedFrom` API.
15
15
 
16
16
  ## When to Use
17
17
 
18
- Apply this rule when a Harper application needs to cache responses from an external API, microservice, or database to avoid repeated slow or expensive upstream calls. Use it whenever you need to define TTL-based cache expiration, observe ETag-based conditional responses, or manually invalidate cached entries.
18
+ Apply this rule when an application needs to wrap an external API, microservice, or database with a fast local cache. Use it when you need to define TTL-based cache expiration, connect an upstream data source to a Harper table, or implement on-demand cache invalidation.
19
19
 
20
20
  ## How It Works
21
21
 
22
- 1. **Define a cache table with `expiration`**: In `schema.graphql`, add the `expiration` argument to `@table`. The value is in seconds. Any record older than this threshold is considered stale and will be re-fetched on next access.
22
+ 1. **Define a cache table with `expiration`**: Add the `expiration` argument to the `@table` directive in `schema.graphql`. The value is in seconds. When a record becomes stale, the next request fetches a fresh copy from the upstream source.
23
23
 
24
24
  ```graphql
25
25
  type JokeCache @table(expiration: 60) @export {
@@ -29,7 +29,7 @@ Apply this rule when a Harper application needs to cache responses from an exter
29
29
  }
30
30
  ```
31
31
 
32
- 2. **Wrap the external source in `resources.js`**: Create an object with a `get(id)` method that fetches from the upstream source. Then call `sourcedFrom` on the table to register it.
32
+ 2. **Implement an upstream source object**: In `resources.js`, create an object with a `get(id)` method that fetches data from the external API.
33
33
 
34
34
  ```javascript
35
35
  const jokeAPI = {
@@ -38,18 +38,22 @@ Apply this rule when a Harper application needs to cache responses from an exter
38
38
  return response.json();
39
39
  },
40
40
  };
41
+ ```
42
+
43
+ 3. **Connect the source with `sourcedFrom`**: Call `sourcedFrom` on the table to register the upstream source. Harper will call `jokeAPI.get()` automatically when a record is missing or stale.
41
44
 
45
+ ```javascript
42
46
  tables.JokeCache.sourcedFrom(jokeAPI);
43
47
  ```
44
48
 
45
- Harper's caching behavior after `sourcedFrom` is registered:
46
- - A request arrives for `/JokeCache/1`.
47
- - Harper checks if the record with id `1` exists in `JokeCache` and is not stale.
49
+ Harper's request flow after `sourcedFrom` is registered:
50
+ - Request arrives for `/JokeCache/1`.
51
+ - Harper checks if the record exists and is not stale.
48
52
  - If fresh, Harper returns it immediately.
49
53
  - If missing or stale, Harper calls `jokeAPI.get()`, stores the result in `JokeCache`, and returns it.
50
54
  - Multiple simultaneous requests for the same missing or stale record wait on a single upstream call — Harper prevents cache stampedes automatically.
51
55
 
52
- 3. **Configure plugins in `config.yaml`**: Enable the schema, REST API, and JS resource plugins.
56
+ 4. **Configure plugins in `config.yaml`**: Enable `graphqlSchema`, `rest`, and `jsResource`.
53
57
 
54
58
  ```yaml
55
59
  graphqlSchema:
@@ -59,29 +63,7 @@ Apply this rule when a Harper application needs to cache responses from an exter
59
63
  files: 'resources.js'
60
64
  ```
61
65
 
62
- 4. **Observe caching via ETags**: Harper automatically computes an ETag from the record's last-modified timestamp. On the first request you receive a `200` with an `etag` header. Pass that value back in `If-None-Match` on subsequent requests; Harper returns `304 Not Modified` with an empty body if the record is unchanged.
63
-
64
- ```bash
65
- curl -i 'http://localhost:9926/JokeCache/1' \
66
- -H 'If-None-Match: "abCDefGHij"'
67
- ```
68
-
69
- 5. **Force a cache bypass**: Send `Cache-Control: no-cache` to make Harper skip the local cache and always call the upstream source, regardless of TTL.
70
-
71
- ```bash
72
- curl -i 'http://localhost:9926/JokeCache/1' \
73
- -H 'Cache-Control: no-cache'
74
- ```
75
-
76
- 6. **Invalidate a cache entry on demand**: Remove `@export` from the schema type, then export a class of the same name in `resources.js` that extends the table and implements a `post` handler calling `this.invalidate(target)`.
77
-
78
- ```graphql
79
- type JokeCache @table(expiration: 60) {
80
- id: ID @primaryKey
81
- setup: String
82
- punchline: String
83
- }
84
- ```
66
+ 5. **Implement on-demand invalidation**: To invalidate a cache entry before its TTL expires, export a class extending the table and call `this.invalidate(target)` in a `post` handler. Remove `@export` from the schema when using this pattern the exported class provides the endpoint.
85
67
 
86
68
  ```javascript
87
69
  export class JokeCache extends tables.JokeCache {
@@ -95,34 +77,25 @@ Apply this rule when a Harper application needs to cache responses from an exter
95
77
  }
96
78
  ```
97
79
 
98
- Trigger invalidation with a `POST`:
80
+ Update the schema to remove `@export`:
99
81
 
100
- ```bash
101
- curl -X POST 'http://localhost:9926/JokeCache/1' \
102
- -H 'Content-Type: application/json' \
103
- -d '{"action": "invalidate"}'
82
+ ```graphql
83
+ type JokeCache @table(expiration: 60) {
84
+ id: ID @primaryKey
85
+ setup: String
86
+ punchline: String
87
+ }
104
88
  ```
105
89
 
106
- The next `GET /JokeCache/1` will fetch fresh data from the upstream source regardless of TTL.
107
-
108
90
  ## Examples
109
91
 
110
- Complete `schema.graphql` and `resources.js` for a cached external API with on-demand invalidation:
111
-
112
- ```graphql
113
- type JokeCache @table(expiration: 60) {
114
- id: ID @primaryKey
115
- setup: String
116
- punchline: String
117
- }
118
- ```
92
+ **Complete `resources.js`**:
119
93
 
120
94
  ```javascript
121
95
  // resources.js
122
96
 
123
97
  const jokeAPI = {
124
- async get() {
125
- const id = this.getId();
98
+ async get(id) {
126
99
  const response = await fetch(`https://official-joke-api.appspot.com/jokes/${id}`);
127
100
  return response.json();
128
101
  },
@@ -141,23 +114,56 @@ export class JokeCache extends tables.JokeCache {
141
114
  }
142
115
  ```
143
116
 
144
- First request — cache miss, upstream is called, `200` returned:
117
+ **Complete `schema.graphql`**:
145
118
 
146
- ```bash
147
- curl -i 'http://localhost:9926/JokeCache/1'
119
+ ```graphql
120
+ type JokeCache @table(expiration: 60) {
121
+ id: ID @primaryKey
122
+ setup: String
123
+ punchline: String
124
+ }
148
125
  ```
149
126
 
150
- Second request with ETag — cache hit, `304 Not Modified`:
127
+ **Fetch a cached record**:
151
128
 
152
- ```bash
153
- curl -i 'http://localhost:9926/JokeCache/1' \
154
- -H 'If-None-Match: "abCDefGHij"'
129
+ ```javascript
130
+ const response = await fetch('http://localhost:9926/JokeCache/1');
131
+ console.log(response.status); // 200
132
+ const etag = response.headers.get('etag'); // e.g. "abCDefGHij"
133
+ const joke = await response.json();
134
+ ```
135
+
136
+ **Use ETag for conditional requests** (returns `304 Not Modified` if unchanged):
137
+
138
+ ```javascript
139
+ const second = await fetch('http://localhost:9926/JokeCache/1', {
140
+ headers: { 'If-None-Match': etag },
141
+ });
142
+ console.log(second.status); // 304
143
+ ```
144
+
145
+ **Bypass the cache with `Cache-Control: no-cache`**:
146
+
147
+ ```javascript
148
+ const response = await fetch('http://localhost:9926/JokeCache/1', {
149
+ headers: { 'Cache-Control': 'no-cache' },
150
+ });
151
+ ```
152
+
153
+ **Trigger invalidation via POST**:
154
+
155
+ ```javascript
156
+ await fetch('http://localhost:9926/JokeCache/1', {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify({ action: 'invalidate' }),
160
+ });
155
161
  ```
156
162
 
157
163
  ## Notes
158
164
 
159
165
  - `expiration` is measured in seconds. Harper also supports separate `eviction` and `scanInterval` arguments on `@table` for fine-grained control over physical record removal.
160
- - The `@export` directive on the schema type is not required when you export a Resource class of the same name from `resources.js` — the class export serves as the endpoint registration. See [custom-resources.md](custom-resources.md) for details on building Resource classes.
161
- - Harper's REST layer automatically exposes `@export`-ed tables and Resource classes as HTTP endpoints. See [automatic-apis.md](automatic-apis.md) for how endpoints are structured and named.
162
- - ETag values include their double quotes as part of the value — include them verbatim when passing the value in `If-None-Match`.
163
- - `sourcedFrom` must be called after the table reference (`tables.JokeCache`) is available, which is guaranteed when the call is at the top level of `resources.js`.
166
+ - ETags are automatically computed from a record's last-modified timestamp. Include the double quotes when passing an ETag back in `If-None-Match` — they are part of the value.
167
+ - Exporting a class with the same name as a table (e.g., `export class JokeCache extends tables.JokeCache`) registers it as the HTTP endpoint for that table; `@export` in the schema is not required separately.
168
+ - For defining custom upstream source behavior beyond a simple `get`, see [custom-resources.md](custom-resources.md).
169
+ - For details on how `@table` and `@export` expose REST endpoints automatically, see [automatic-apis.md](automatic-apis.md).
@@ -2,43 +2,164 @@
2
2
  name: custom-resources
3
3
  description: How to define custom REST endpoints with JavaScript or TypeScript in Harper.
4
4
  metadata:
5
- mode: synthesized
5
+ mode: generate
6
+ sources:
7
+ - reference/v5/resources/overview.md#Custom External Data Source
8
+ - reference/v5/resources/overview.md#Exporting Resources as Endpoints
9
+ - reference/v5/components/javascript-environment.md#Module Loading
10
+ sourceCommit: f37a8c4021e20d5c74c1d339a6b6c8c196b5603e
11
+ inputHash: df69870433c0b3e5
6
12
  ---
7
13
 
8
14
  # Custom Resources
9
15
 
10
- Instructions for the agent to follow when creating custom resources in Harper.
16
+ Instructions for the agent to follow when defining custom REST endpoints with JavaScript or TypeScript in Harper.
11
17
 
12
18
  ## When to Use
13
19
 
14
- Use this skill when the automatic CRUD operations provided by `@table @export` are insufficient, and you need custom logic, third-party API integration, or specialized data handling for your REST endpoints.
20
+ 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.
15
21
 
16
22
  ## How It Works
17
23
 
18
- 1. **Check if a Custom Resource is Necessary**: Verify if [Automatic APIs](./automatic-apis.md) or [Extending Tables](./extending-tables.md) can satisfy the requirement first.
19
- 2. **Create the Resource File**: Create a `.ts` or `.js` file in the directory specified by `jsResource` in `config.yaml` (typically `resources/`).
20
- 3. **Define the Resource Class**: Export a class extending `Resource` from `harper` and define **static** methods for the HTTP verbs you handle. In Harper 5 the static methods are the HTTP handlers (mapped 1:1 to verbs); they receive a pre-parsed `RequestTarget`, and write handlers also receive the request body as an awaitable `data` argument:
24
+ 1. **Import `Resource` from `harper`**: Always import from the `harper` package rather than relying on globals.
21
25
 
22
- ```typescript
23
- import { Resource } from 'harper';
26
+ ```javascript
27
+ import { tables, Resource } from 'harper';
28
+ ```
29
+
30
+ 2. **Define a class that `extends Resource`**: Implement HTTP methods as `static` methods. Each method receives a `target` object.
31
+
32
+ ```javascript
33
+ export class CustomEndpoint extends Resource {
34
+ static get(target) {
35
+ return {
36
+ data: doSomething(),
37
+ };
38
+ }
39
+ }
40
+ ```
41
+
42
+ 3. **Use `async` static methods for external calls**: Await fetch or other async operations inside `static` handlers.
24
43
 
25
- export class MyResource extends Resource {
26
- // v5 handlers are static and map 1:1 to HTTP verbs.
27
- static async get(target: any) {
28
- return { message: 'Hello from custom GET!' };
44
+ ```javascript
45
+ export class MyExternalData extends Resource {
46
+ static async get(target) {
47
+ const response = await fetch(`https://api.example.com/${target.id}`);
48
+ return response.json();
49
+ }
50
+
51
+ static async put(target, data) {
52
+ return fetch(`https://api.example.com/${target.id}`, {
53
+ method: 'PUT',
54
+ body: JSON.stringify(await data),
55
+ });
29
56
  }
30
57
  }
31
58
  ```
32
59
 
33
- 4. **Implement HTTP Methods**: Add static methods (`get`, `post`, `put`, `patch`, or `delete`) to handle the corresponding requests. Read/delete handlers receive `(target)`; write handlers receive `(target, data)` where `data` is awaitable.
34
- 5. **Route Nesting and Naming**: You can control the URL structure by how you export your resources:
35
- - **Direct Class Export**: `export class Foo extends Resource` creates endpoints at `/Foo/`. Class names are case-sensitive in the URL.
36
- - **Nested Objects**: `export const Bar = { Foo };` creates endpoints at `/Bar/Foo/`.
37
- - **Lowercase and Hyphens**: Use object keys to define custom paths: `export const bar = { 'foo-baz': Foo };` exposes endpoints at `/bar/foo-baz/`.
38
- 6. **Access Tables (Optional)**: Import and use the `tables` object to interact with your data:
39
- ```typescript
40
- import { tables } from 'harper';
41
- // ... inside a method
42
- const results = await tables.MyTable.list();
60
+ 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.
61
+
62
+ | Export form | URL | Notes |
63
+ | ------------------------------------------- | --------------- | --------------------------------------------------------------- |
64
+ | `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment. Case-sensitive. |
65
+ | `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
66
+ | `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
67
+ | `export { Foo as '/widget/:id' }` | `/widget/:id` | Rename the export to set the path directly. |
68
+ | `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
69
+ | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
70
+
71
+ URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
72
+
73
+ 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>`.
74
+
75
+ ```javascript
76
+ export class Widget extends Resource {
77
+ static path = '/widget/:id/action/:action';
78
+ static get(target) {
79
+ return { id: target.id, action: target.action };
80
+ }
81
+ }
82
+ ```
83
+
84
+ 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.
85
+
86
+ 6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` when the path cannot be known at export time.
87
+
88
+ ```javascript
89
+ server.resources.set('my-path', Foo);
43
90
  ```
44
- 7. **Configure Loading**: Ensure `config.yaml` points to your resource files (e.g., `jsResource: { files: 'resources/*.ts' }`).
91
+
92
+ 7. **Optionally source a table from a custom resource**: Use the resource as a caching layer for a local table.
93
+ ```javascript
94
+ tables.MyCache.sourcedFrom(MyExternalData);
95
+ ```
96
+
97
+ ## Examples
98
+
99
+ ### External API wrapper with GET and PUT
100
+
101
+ ```javascript
102
+ import { tables, Resource } from 'harper';
103
+
104
+ export class MyExternalData extends Resource {
105
+ static async get(target) {
106
+ const response = await fetch(`https://api.example.com/${target.id}`);
107
+ return response.json();
108
+ }
109
+
110
+ static async put(target, data) {
111
+ return fetch(`https://api.example.com/${target.id}`, {
112
+ method: 'PUT',
113
+ body: JSON.stringify(await data),
114
+ });
115
+ }
116
+ }
117
+
118
+ // Use as a cache source for a local table
119
+ tables.MyCache.sourcedFrom(MyExternalData);
120
+ ```
121
+
122
+ ### Path parameters with `static path`
123
+
124
+ ```javascript
125
+ import { Resource } from 'harper';
126
+
127
+ export class Widget extends Resource {
128
+ // GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
129
+ static path = '/widget/:id/action/:action';
130
+ static get(target) {
131
+ return { id: target.id, action: target.action };
132
+ }
133
+ }
134
+
135
+ export class Files extends Resource {
136
+ // GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
137
+ static path = '/files/*rest';
138
+ static get(target) {
139
+ return { path: target.rest };
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Programmatic registration
145
+
146
+ ```javascript
147
+ import { Resource } from 'harper';
148
+
149
+ export class Foo extends Resource {
150
+ static get(target) {
151
+ return { data: doSomething() };
152
+ }
153
+ }
154
+
155
+ server.resources.set('my-path', Foo);
156
+ ```
157
+
158
+ ## Notes
159
+
160
+ - A bare `*` wildcard (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
161
+ - 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.
162
+ - 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.
163
+ - If a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
164
+ - Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
165
+ - Harper runs as a single process — `tables`, `databases`, and other APIs are the same live, process-wide objects regardless of which component accesses them.
@@ -2,34 +2,164 @@
2
2
  name: defining-relationships
3
3
  description: How to define and use relationships between tables in Harper using GraphQL.
4
4
  metadata:
5
- mode: synthesized
5
+ mode: generate
6
+ sources:
7
+ - reference/v5/database/schema.md#Relationships
8
+ - reference/v5/rest/querying.md#Relationships and Joins
9
+ sourceCommit: 4fe4c9c95e0974eaa77032f6f10e36fbd8ec64ac
10
+ inputHash: 6953f507f0cde0f7
6
11
  ---
7
12
 
8
- # Defining Relationships
13
+ # Defining Relationships Between Tables in Harper
9
14
 
10
- Instructions for the agent to follow when defining relationships between Harper tables.
15
+ Instructions for the agent to follow when defining and querying relationships between tables in Harper using the `@relationship` directive.
11
16
 
12
17
  ## When to Use
13
18
 
14
- Use this skill when you need to link data across different tables, enabling automatic joins and efficient related-data fetching via REST APIs.
19
+ Apply this rule whenever a schema requires linking two tables via a foreign key — for example, modeling shows and networks, products and brands, or orders and items. Use it when queries need to filter or select nested related records using dot-syntax.
15
20
 
16
21
  ## How It Works
17
22
 
18
- 1. **Identify the Relationship Type**: Determine if it's one-to-one, many-to-one, or one-to-many.
19
- 2. **Use the `@relationship` Directive**: Apply it to a field in your GraphQL schema.
20
- - **Many-to-One (Current table holds FK)**: Use `from`.
21
- ```graphql
22
- type Book @table @export {
23
- authorId: ID
24
- author: Author @relationship(from: "authorId")
25
- }
26
- ```
27
- - **One-to-Many (Related table holds FK)**: Use `to` and an array type.
28
- ```graphql
29
- type Author @table @export {
30
- books: [Book] @relationship(to: "authorId")
31
- }
32
- ```
33
- 3. **Query with Relationships**: Use dot syntax in REST API calls for filtering or the `select()` operator for including related data.
34
- - Example Filter: `GET /Book/?author.name=Harper`
35
- - Example Select: `GET /Author/?select(name,books(title))`
23
+ 1. **Use `@relationship(from: attribute)` for many-to-one or many-to-many**: Place this on a field in the current table when the foreign key lives in this table and references the primary key of the target table.
24
+
25
+ ```graphql
26
+ type RealityShow @table @export {
27
+ id: Long @primaryKey
28
+ networkId: Long @indexed
29
+ network: Network @relationship(from: networkId)
30
+ title: String @indexed
31
+ }
32
+
33
+ type Network @table @export {
34
+ id: Long @primaryKey
35
+ name: String @indexed
36
+ }
37
+ ```
38
+
39
+ For a many-to-many relationship, make the foreign key an array:
40
+
41
+ ```graphql
42
+ type RealityShow @table @export {
43
+ id: Long @primaryKey
44
+ networkIds: [Long] @indexed
45
+ networks: [Network] @relationship(from: networkIds)
46
+ }
47
+ ```
48
+
49
+ 2. **Use `@relationship(to: attribute)` for one-to-many or many-to-many**: Place this on a field in the current table when the foreign key lives in the target table and references the primary key of this table. The result type must be an array.
50
+
51
+ ```graphql
52
+ type Network @table @export {
53
+ id: Long @primaryKey
54
+ name: String @indexed
55
+ shows: [RealityShow] @relationship(to: networkId)
56
+ }
57
+ ```
58
+
59
+ 3. **Use `@relationship(from: attribute, to: attribute)` for foreign key to foreign key joins**: Specify both `from` and `to` when neither side uses the primary key. This is useful for joining on non-primary-key attributes.
60
+
61
+ ```graphql
62
+ type OrderItem @table @export {
63
+ id: Long @primaryKey
64
+ orderId: Long @indexed
65
+ productSku: Long @indexed
66
+ product: Product @relationship(from: productSku, to: sku)
67
+ }
68
+
69
+ type Product @table @export {
70
+ id: Long @primaryKey
71
+ sku: Long @indexed
72
+ name: String
73
+ }
74
+ ```
75
+
76
+ 4. **Query across relationships using dot-syntax**: Filter by related table attributes using chained dot notation. This behaves as an INNER JOIN.
77
+
78
+ ```
79
+ GET /RealityShow?network.name=Bravo
80
+ GET /Product/?brand.name=Microsoft
81
+ GET /Brand/?products.name=Keyboard
82
+ ```
83
+
84
+ 5. **Select nested relationship fields with `select()`**: Relationship attributes are not included by default. Use `select()` to include them in results. When selecting without a filter on the related table, this acts as a LEFT JOIN — the relationship property is omitted if the foreign key is null or references a non-existent record.
85
+
86
+ ```
87
+ GET /Product/?brand.name=Microsoft&select(name,brand)
88
+ GET /Product/?brand.name=Microsoft&select(name,brand{name})
89
+ GET /Product/?name=Keyboard&select(name,brand{name,id})
90
+ ```
91
+
92
+ ## Examples
93
+
94
+ **Many-to-one relationship** — a show belongs to a network:
95
+
96
+ ```graphql
97
+ type RealityShow @table @export {
98
+ id: Long @primaryKey
99
+ networkId: Long @indexed
100
+ network: Network @relationship(from: networkId)
101
+ title: String @indexed
102
+ }
103
+
104
+ type Network @table @export {
105
+ id: Long @primaryKey
106
+ name: String @indexed
107
+ }
108
+ ```
109
+
110
+ Query:
111
+
112
+ ```
113
+ GET /RealityShow?network.name=Bravo
114
+ ```
115
+
116
+ **One-to-many relationship** — a network has many shows:
117
+
118
+ ```graphql
119
+ type Network @table @export {
120
+ id: Long @primaryKey
121
+ name: String @indexed
122
+ shows: [RealityShow] @relationship(to: networkId)
123
+ }
124
+ ```
125
+
126
+ **Many-to-many with array foreign keys** — a product has multiple resellers:
127
+
128
+ ```graphql
129
+ type Product @table @export {
130
+ id: Long @primaryKey
131
+ name: String
132
+ resellerIds: [Long] @indexed
133
+ resellers: [Reseller] @relationship(from: resellerIds)
134
+ }
135
+ ```
136
+
137
+ Query with nested select:
138
+
139
+ ```
140
+ GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
141
+ ```
142
+
143
+ **Foreign key to foreign key join** — order item joined on SKU:
144
+
145
+ ```graphql
146
+ type OrderItem @table @export {
147
+ id: Long @primaryKey
148
+ orderId: Long @indexed
149
+ productSku: Long @indexed
150
+ product: Product @relationship(from: productSku, to: sku)
151
+ }
152
+
153
+ type Product @table @export {
154
+ id: Long @primaryKey
155
+ sku: Long @indexed
156
+ name: String
157
+ }
158
+ ```
159
+
160
+ ## Notes
161
+
162
+ - The `@relationship` directive requires the referenced attribute to be `@indexed` on the foreign key side.
163
+ - Self-referential relationships are supported, enabling parent-child hierarchies within a single table.
164
+ - The array order of foreign key values (e.g., `resellerIds`) is preserved when resolving many-to-many relationships.
165
+ - When using `select()` without a filter on the related table, the join behaves as a LEFT JOIN — missing or null foreign keys result in the relationship property being omitted rather than causing an error.