@harperfast/template-vue-ts-studio 1.9.1 → 1.10.0

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 (24) hide show
  1. package/.agents/skills/harper-best-practices/AGENTS.md +770 -254
  2. package/.agents/skills/harper-best-practices/rules/caching.md +68 -62
  3. package/.agents/skills/harper-best-practices/rules/custom-resources.md +106 -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 +51 -7
  8. package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
  9. package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +96 -16
  10. package/.agents/skills/harper-best-practices/rules/vector-indexing.md +59 -29
  11. package/.agents/skills/harper-best-practices/rules.manifest.yaml +83 -6
  12. package/agent/skills/harper-best-practices/AGENTS.md +770 -254
  13. package/agent/skills/harper-best-practices/rules/caching.md +68 -62
  14. package/agent/skills/harper-best-practices/rules/custom-resources.md +106 -23
  15. package/agent/skills/harper-best-practices/rules/defining-relationships.md +152 -22
  16. package/agent/skills/harper-best-practices/rules/extending-tables.md +90 -21
  17. package/agent/skills/harper-best-practices/rules/handling-binary-data.md +103 -23
  18. package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +51 -7
  19. package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +87 -64
  20. package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +96 -16
  21. package/agent/skills/harper-best-practices/rules/vector-indexing.md +59 -29
  22. package/agent/skills/harper-best-practices/rules.manifest.yaml +83 -6
  23. package/package.json +1 -1
  24. 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`.
@@ -74,13 +74,13 @@ Use these exact strings — incorrect comparator names will silently fail or err
74
74
 
75
75
  ### Query Object Parameters
76
76
 
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 |
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 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) |
83
+ | `sort` | Object with `attribute`, `descending` (bool), and optional `next` for secondary sort |
84
84
 
85
85
  ### Examples
86
86
 
@@ -129,6 +129,50 @@ for await (const record of tables.Product.search({
129
129
  })) { ... }
130
130
  ```
131
131
 
132
+ ## Selecting Related Data
133
+
134
+ 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)).
135
+
136
+ **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:
137
+
138
+ ```javascript
139
+ for await (const book of tables.Book.search({
140
+ conditions: [{ attribute: 'id', value: 42 }],
141
+ select: ['id', 'title', 'author'], // `author` is a relationship field
142
+ })) {
143
+ console.log(book.author.name); // the full related Author record
144
+ }
145
+ ```
146
+
147
+ **Partial related record** — use an object `{ name, select }` to choose which fields of the related record to return. Unselected fields are omitted:
148
+
149
+ ```javascript
150
+ for await (const book of tables.Book.search({
151
+ conditions: [{ attribute: ['author', 'name'], comparator: 'equals', value: 'Harper' }],
152
+ select: ['id', 'title', { name: 'author', select: ['name'] }],
153
+ })) {
154
+ // book.author.name is present; other Author fields are undefined
155
+ }
156
+ ```
157
+
158
+ **Nesting** — a `select` inside an object entry may itself contain more `{ name, select }` objects, traversing multiple relationships in one query:
159
+
160
+ ```javascript
161
+ select: [
162
+ 'id',
163
+ 'name',
164
+ {
165
+ name: 'segments',
166
+ select: ['id', 'name', { name: 'client', select: ['id', 'name'] }],
167
+ },
168
+ ],
169
+ ```
170
+
171
+ Notes:
172
+
173
+ - A to-many relationship resolves to an array of records; depending on access pattern you may need to `await` the property before iterating it.
174
+ - 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.
175
+
132
176
  ## Cautions
133
177
 
134
178
  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.
@@ -9,11 +9,11 @@ metadata:
9
9
  - reference/v5/database/schema.md#Overview
10
10
  - reference/v5/database/schema.md#Type Directives
11
11
  - reference/v5/database/schema.md#Field Directives
12
- sourceCommit: b7fbddadd42eb4487190b650a9abc4bcfeef5819
13
- inputHash: 4faa3baed7cfa854
12
+ sourceCommit: 4fe4c9c95e0974eaa77032f6f10e36fbd8ec64ac
13
+ inputHash: 15b9c72decc1f05d
14
14
  ---
15
15
 
16
- # Schema Design and Tooling
16
+ # Schema Design and GraphQL Tooling
17
17
 
18
18
  Instructions for the agent to follow when designing Harper schemas, applying core directives, and configuring GraphQL tooling.
19
19
 
@@ -23,7 +23,18 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
23
23
 
24
24
  ## How It Works
25
25
 
26
- 1. **Create a GraphQL schema file** with Harper-specific directives. Name it (e.g., `schema.graphql`) and place it in your component directory.
26
+ 1. **Create a schema file** using standard GraphQL type definitions with Harper-specific directives. Name it (e.g., `schema.graphql`) and place it in your component directory.
27
+
28
+ 2. **Register the schema** in the component's `config.yaml` using the `graphqlSchema` plugin:
29
+
30
+ ```yaml
31
+ graphqlSchema:
32
+ files: 'schema.graphql'
33
+ ```
34
+
35
+ Both plugins and applications can specify schemas.
36
+
37
+ 3. **Mark types as tables** with `@table`. The type name becomes the table name by default:
27
38
 
28
39
  ```graphql
29
40
  type Dog @table {
@@ -32,23 +43,36 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
32
43
  breed: String
33
44
  age: Int
34
45
  }
46
+ ```
35
47
 
48
+ 4. **Designate a primary key** with `@primaryKey` on exactly one field per type. Primary keys must be unique; duplicate-key inserts are rejected. If no primary key is provided on insert, Harper auto-generates one:
49
+ - `String` or `ID` → UUID string
50
+ - `Int`, `Long`, or `Any` → auto-incrementing integer
51
+
52
+ Use `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
53
+
54
+ 5. **Add secondary indexes** with `@indexed` on any field that will be used for filtering in REST queries, SQL, or NoSQL operations:
55
+
56
+ ```graphql
36
57
  type Breed @table {
37
58
  id: Long @primaryKey
38
59
  name: String @indexed
39
60
  }
40
61
  ```
41
62
 
42
- 2. **Register the schema in `config.yaml`** using the `graphqlSchema` plugin key:
63
+ If the field value is an array, each element is individually indexed. Null values are indexed by default.
43
64
 
44
- ```yaml
45
- graphqlSchema:
46
- files: 'schema.graphql'
65
+ 6. **Expose a table as an external endpoint** with `@export`. The optional `name` parameter sets the URL path segment:
66
+
67
+ ```graphql
68
+ type MyTable @table @export(name: "my-table") {
69
+ id: Long @primaryKey
70
+ }
47
71
  ```
48
72
 
49
- Both plugins and applications can specify schemas this way.
73
+ Without `name`, the type name is used as the path segment.
50
74
 
51
- 3. **Mark every table type with `@table`**. The type name becomes the table name by default. Use optional arguments to override behavior:
75
+ 7. **Configure `@table` arguments** as needed for database placement, expiration, eviction, and replication:
52
76
 
53
77
  | Argument | Type | Default | Description |
54
78
  | -------------- | --------- | ----------------------------- | ------------------------------------------------------------- |
@@ -59,32 +83,13 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
59
83
  | `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
60
84
  | `replicate` | `Boolean` | `true` | Enable replication of this table |
61
85
 
62
- 4. **Designate a primary key on every table** using `@primaryKey`. Primary keys must be unique; duplicate-key inserts are rejected. If no key is provided on insert, Harper auto-generates one:
63
- - `String` or `ID` UUID string
64
- - `Int`, `Long`, or `Any` auto-incrementing integer
65
-
66
- Prefer `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
67
-
68
- 5. **Index fields that need fast querying** with `@indexed`. This is required for filtering by that attribute in REST queries, SQL, or NoSQL operations. If the field value is an array, each element is individually indexed.
69
-
70
- ```graphql
71
- type Product @table {
72
- id: Long @primaryKey
73
- category: String @indexed
74
- price: Float @indexed
75
- }
76
- ```
77
-
78
- 6. **Expose a table as an external resource endpoint** with `@export`. This makes the table accessible via REST, MQTT, and other interfaces. The optional `name` parameter sets the URL path segment; without it, the type name is used.
79
-
80
- ```graphql
81
- type MyTable @table @export(name: "my-table") {
82
- id: Long @primaryKey
83
- }
84
- ```
85
-
86
- 7. **Restrict extra properties** with `@sealed` when records must not include attributes beyond those declared. By default, Harper allows additional properties.
86
+ 8. **Apply additional field directives** where needed:
87
+ - `@createdTime` auto-assigns Unix epoch milliseconds on record creation
88
+ - `@updatedTime` auto-assigns Unix epoch milliseconds on each update
89
+ - `@embed(source: "fieldName", model: "modelName")` — computes an embedding vector on write; attribute type must be `[Float]`
90
+ - `@hidden` suppresses the field from MCP tool descriptors and OpenAPI documents (not an access-control mechanism)
87
91
 
92
+ 9. **Restrict extra properties** with `@sealed` if records must not include properties beyond those declared:
88
93
  ```graphql
89
94
  type StrictRecord @table @sealed {
90
95
  id: Long @primaryKey
@@ -92,70 +97,88 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
92
97
  }
93
98
  ```
94
99
 
95
- 8. **Configure expiration, eviction, and scan behavior** together when building caching tables. These three arguments control the full record lifecycle:
96
- - `expiration` — record becomes stale; next request triggers a source fetch
97
- - `eviction` — additional time after `expiration` before physical removal
98
- - `scanInterval` — how often Harper scans for records to evict; clock-aligned, not startup-aligned
99
-
100
100
  ## Examples
101
101
 
102
- **Caching table with tuned expiration:**
102
+ **Minimal two-table schema:**
103
103
 
104
104
  ```graphql
105
- # Expire after 5 minutes, evict after 1 hour, scan every 10 minutes
106
- type WeatherCache @table(expiration: 300, eviction: 3300, scanInterval: 600) {
107
- id: ID @primaryKey
108
- temperature: Float
105
+ type Dog @table {
106
+ id: Long @primaryKey
107
+ name: String
108
+ breed: String
109
+ age: Int
109
110
  }
110
- ```
111
-
112
- **Table in a named database with expiration and an indexed field:**
113
111
 
114
- ```graphql
115
- type Event @table(database: "analytics", expiration: 86400) {
112
+ type Breed @table {
116
113
  id: Long @primaryKey
117
114
  name: String @indexed
118
115
  }
119
116
  ```
120
117
 
121
- **Session cache with auto-expiry:**
118
+ **Table with expiration, eviction, and custom scan interval:**
122
119
 
123
120
  ```graphql
124
- type Session @table(expiration: 3600) {
125
- id: Long @primaryKey
126
- userId: String
121
+ # Expire after 5 minutes, evict after 1 hour, scan every 10 minutes
122
+ type WeatherCache @table(expiration: 300, eviction: 3300, scanInterval: 600) {
123
+ id: ID @primaryKey
124
+ temperature: Float
127
125
  }
128
126
  ```
129
127
 
130
- **Table with audit timestamps:**
128
+ **Exported table with timestamps and a hidden field:**
131
129
 
132
130
  ```graphql
133
- type Order @table @export(name: "orders") {
131
+ type Customer @table @export(name: "customers") {
134
132
  id: Long @primaryKey
133
+ name: String @indexed
135
134
  createdAt: Long @createdTime
136
135
  updatedAt: Long @updatedTime
137
- status: String @indexed
136
+
137
+ """
138
+ Internal — do not surface to external consumers.
139
+ """
140
+ creditScore: Int @hidden
138
141
  }
139
142
  ```
140
143
 
141
- **Overriding the table name and disabling replication:**
144
+ **Multiple `@table` argument combinations:**
142
145
 
143
146
  ```graphql
147
+ # Override table name
144
148
  type Product @table(table: "products") {
145
149
  id: Long @primaryKey
146
- name: String
147
150
  }
148
151
 
152
+ # Place in a specific database
153
+ type Order @table(database: "commerce") {
154
+ id: Long @primaryKey
155
+ }
156
+
157
+ # Auto-expire records after 1 hour
158
+ type Session @table(expiration: 3600) {
159
+ id: Long @primaryKey
160
+ userId: String
161
+ }
162
+
163
+ # Disable replication
149
164
  type LocalRecord @table(replicate: false) {
150
165
  id: Long @primaryKey
151
166
  value: String
152
167
  }
168
+
169
+ # Combine multiple arguments
170
+ type Event @table(database: "analytics", expiration: 86400) {
171
+ id: Long @primaryKey
172
+ name: String @indexed
173
+ }
153
174
  ```
154
175
 
155
176
  ## Notes
156
177
 
157
- - Use unique `database` names in plugins and applications to avoid table naming collisions, since all tables default to the `"data"` database.
158
- - Eviction removes non-indexed record data but does **not** remove a record from its secondary indexes. Indexes remain functional for evicted records; Harper fetches the full record from the source on demand when a query matches an evicted record.
159
- - `scanInterval` is clock-aligned to the server's local timezone. The server's startup time does not affect when eviction runs.
160
- - If replication is disabled on a table and later re-enabled, it will not catch up on writes made while replication was disabled.
161
- - Null values are indexed by `@indexed` fields, enabling queries such as `GET /Product/?category=null`.
178
+ - All tables default to the `"data"` database. When designing plugins or applications, use unique database names to avoid table naming collisions.
179
+ - Schemas are flexible by default records may include properties beyond those declared. Use `@sealed` to prevent this.
180
+ - `expiration` marks a record stale; `eviction` controls how long after expiration the record is physically removed. Eviction does not remove records from secondary indexes — Harper fetches the full record on demand if an evicted record matches a query.
181
+ - `scanInterval` is clock-aligned to the server's local timezone, not startup-aligned. The eviction schedule is deterministic across restarts.
182
+ - If replication is disabled on a table and later re-enabled, writes made during the disabled period are not replicated retroactively.
183
+ - `@hidden` (on types or fields) is a metadata-visibility directive only. Use `attribute_permissions` on roles to enforce data access control.
184
+ - Disabling replication (`replicate: false`) and re-enabling it later will not catch up on writes made while replication was disabled.