@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.
- package/.agents/skills/harper-best-practices/AGENTS.md +916 -338
- package/.agents/skills/harper-best-practices/rules/caching.md +68 -62
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +144 -23
- package/.agents/skills/harper-best-practices/rules/defining-relationships.md +152 -22
- package/.agents/skills/harper-best-practices/rules/extending-tables.md +90 -21
- package/.agents/skills/harper-best-practices/rules/handling-binary-data.md +103 -23
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +143 -91
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
- package/.agents/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +97 -16
- package/.agents/skills/harper-best-practices/rules/vector-indexing.md +59 -29
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +109 -7
- package/agent/skills/harper-best-practices/AGENTS.md +916 -338
- package/agent/skills/harper-best-practices/rules/caching.md +68 -62
- package/agent/skills/harper-best-practices/rules/custom-resources.md +144 -23
- package/agent/skills/harper-best-practices/rules/defining-relationships.md +152 -22
- package/agent/skills/harper-best-practices/rules/extending-tables.md +90 -21
- package/agent/skills/harper-best-practices/rules/handling-binary-data.md +103 -23
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +143 -91
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
- package/agent/skills/harper-best-practices/rules/serving-web-content.md +28 -0
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +97 -16
- package/agent/skills/harper-best-practices/rules/vector-indexing.md +59 -29
- package/agent/skills/harper-best-practices/rules.manifest.yaml +109 -7
- package/package.json +1 -1
- 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:
|
|
9
|
-
inputHash:
|
|
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
|
|
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
|
|
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`**:
|
|
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. **
|
|
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
|
|
46
|
-
-
|
|
47
|
-
- Harper checks if the record
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
Update the schema to remove `@export`:
|
|
99
81
|
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 `
|
|
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
|
-
|
|
117
|
+
**Complete `schema.graphql`**:
|
|
145
118
|
|
|
146
|
-
```
|
|
147
|
-
|
|
119
|
+
```graphql
|
|
120
|
+
type JokeCache @table(expiration: 60) {
|
|
121
|
+
id: ID @primaryKey
|
|
122
|
+
setup: String
|
|
123
|
+
punchline: String
|
|
124
|
+
}
|
|
148
125
|
```
|
|
149
126
|
|
|
150
|
-
|
|
127
|
+
**Fetch a cached record**:
|
|
151
128
|
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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. **
|
|
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
|
-
```
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
static async get(target
|
|
28
|
-
|
|
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. **
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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. **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
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.
|