@harperfast/template-react-studio 0.9.3 → 0.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.
- package/AGENTS.md +2 -0
- package/package.json +1 -1
- package/schemas/exampleCat.graphql +6 -0
- package/skills/adding-tables.md +5 -3
- package/skills/extending-tables.md +10 -0
- package/skills/handling-binary-data.md +13 -11
- package/skills/programmatic-table-requests.md +185 -0
- package/skills/serving-web-content.md +82 -0
package/AGENTS.md
CHANGED
|
@@ -7,10 +7,12 @@ This repository contains "skills" that guide AI agents in developing Harper appl
|
|
|
7
7
|
- [Adding Tables](skills/adding-tables.md): Learn how to define schemas and enable automatic REST APIs for your database tables.
|
|
8
8
|
- [Automatic REST APIs](skills/automatic-rest-apis.md): Details on the CRUD endpoints automatically generated for exported tables.
|
|
9
9
|
- [Querying REST APIs](skills/querying-rest-apis.md): How to use filters, operators, sorting, and pagination in REST requests.
|
|
10
|
+
- [Programmatic Table Requests](skills/programmatic-table-requests.md): How to use filters, operators, sorting, and pagination in programmatic table requests.
|
|
10
11
|
- [Custom Resources](skills/custom-resources.md): How to define custom REST endpoints using JavaScript or TypeScript (Note: Paths are case-sensitive).
|
|
11
12
|
- [Extending Table Resources](skills/extending-tables.md): Adding custom logic to automatically generated table resources.
|
|
12
13
|
- [Defining Relationships](skills/defining-relationships.md): Using the `@relationship` directive to link tables.
|
|
13
14
|
- [Real-time Applications](skills/real-time-apps.md): Implementing WebSockets and Pub/Sub for live data updates.
|
|
14
15
|
- [TypeScript Type Stripping](skills/typescript-type-stripping.md): Using TypeScript directly without build tools via Node.js Type Stripping.
|
|
15
16
|
- [Handling Binary Data](skills/handling-binary-data.md): How to store and serve binary data like images or MP3s.
|
|
17
|
+
- [Serving Web Content](skills/serving-web-content): Two ways to serve web content from a Harper application.
|
|
16
18
|
- [Checking Authentication](skills/checking-authentication.md): How to use `this.getCurrentUser()` to verify user identity and roles.
|
package/package.json
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## This table is @export'ed, which means API endpoints are stood up for it automatically.
|
|
2
|
+
type ExampleCat @table @export {
|
|
3
|
+
id: ID @primaryKey # Here we define primary key (must be one)
|
|
4
|
+
name: String # we can define any other attributes here
|
|
5
|
+
tag: String @indexed # we can specify any attributes that should be indexed
|
|
6
|
+
}
|
package/skills/adding-tables.md
CHANGED
|
@@ -21,12 +21,14 @@ To add tables to a Harper database, follow these guidelines:
|
|
|
21
21
|
|
|
22
22
|
### Example
|
|
23
23
|
|
|
24
|
-
In `schemas/
|
|
24
|
+
In `schemas/ExamplePerson.graphql`:
|
|
25
25
|
|
|
26
26
|
```graphql
|
|
27
|
-
type
|
|
27
|
+
type ExamplePerson @table @export {
|
|
28
28
|
id: ID @primaryKey
|
|
29
29
|
name: String
|
|
30
|
-
|
|
30
|
+
tag: String @indexed
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
|
+
|
|
34
|
+
Tip: if you are going to [extend the table](./extending-tables.md) in your resources, then do not `@export` the table from the schema.
|
|
@@ -54,3 +54,13 @@ export class ExamplePeople extends tables.ExamplePeople<ExamplePerson> {
|
|
|
54
54
|
## Important Note
|
|
55
55
|
|
|
56
56
|
When you extend a table resource, HarperDB uses your custom class for all REST API interactions with that table. Make sure to call `super[method]` if you still want the default behavior to occur after your custom logic.
|
|
57
|
+
|
|
58
|
+
Extended tables do not need to be `@export`ed in their schema .graphql.
|
|
59
|
+
|
|
60
|
+
```graphql
|
|
61
|
+
type ExamplePerson @table {
|
|
62
|
+
id: ID @primaryKey
|
|
63
|
+
name: String
|
|
64
|
+
tag: String @indexed
|
|
65
|
+
}
|
|
66
|
+
```
|
|
@@ -8,7 +8,7 @@ In a custom resource or a table resource override, you can intercept the incomin
|
|
|
8
8
|
|
|
9
9
|
### Example
|
|
10
10
|
|
|
11
|
-
Suppose you have a table with a `Blob` field named `data`. You can use `Buffer.from(string, 'base64')` to perform the conversion
|
|
11
|
+
Suppose you have a table with a `Blob` field named `data`. You can use `Buffer.from(string, 'base64')` to perform the conversion to a buffer, and then use Harper's `createBlob` function to turn that buffer into a blob with a type such as `{ type: 'image/jpeg' }`.
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { RequestTargetOrId, Resource } from 'harperdb';
|
|
@@ -17,7 +17,9 @@ export class MyResource extends Resource {
|
|
|
17
17
|
async post(target: RequestTargetOrId, record: any) {
|
|
18
18
|
if (record.data) {
|
|
19
19
|
// Convert base64-encoded string to a Buffer
|
|
20
|
-
record.data = Buffer.from(record.
|
|
20
|
+
record.data = createBlob(Buffer.from(record.artwork, 'base64'), {
|
|
21
|
+
type: 'image/jpeg',
|
|
22
|
+
});
|
|
21
23
|
}
|
|
22
24
|
// Call the super method to perform the actual storage
|
|
23
25
|
return super.post(target, record);
|
|
@@ -29,9 +31,9 @@ export class MyResource extends Resource {
|
|
|
29
31
|
|
|
30
32
|
When you want to serve binary data (like an image or an MP3 file) back to the client, you can return a response object from your resource's `get` method. This object should include the appropriate `status`, `headers`, and the binary data itself in the `body`.
|
|
31
33
|
|
|
32
|
-
### Example:
|
|
34
|
+
### Example: Responding with a JPEG File
|
|
33
35
|
|
|
34
|
-
In this example, we retrieve a
|
|
36
|
+
In this example, we retrieve a thumbnail from the database. If it contains binary data in the `data` field, we return it with the `image/jpeg` content type.
|
|
35
37
|
|
|
36
38
|
```typescript
|
|
37
39
|
import { RequestTarget, RequestTargetOrId, Resource } from 'harperdb';
|
|
@@ -42,23 +44,23 @@ export class TrackResource extends Resource {
|
|
|
42
44
|
if (!id) {
|
|
43
45
|
return super.get(target);
|
|
44
46
|
}
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
+
const thumbnail = await super.get(target);
|
|
48
|
+
if (thumbnail?.data) {
|
|
47
49
|
return {
|
|
48
50
|
status: 200,
|
|
49
51
|
headers: {
|
|
50
|
-
'Content-Type': '
|
|
51
|
-
'Content-Disposition': `inline; filename="${
|
|
52
|
+
'Content-Type': 'image/jpeg',
|
|
53
|
+
'Content-Disposition': `inline; filename="${thumbnail.name}.jpg"`,
|
|
52
54
|
},
|
|
53
|
-
body:
|
|
55
|
+
body: thumbnail.data,
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
|
-
return
|
|
58
|
+
return thumbnail;
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
## Why Use
|
|
63
|
+
## Why Use Blobs?
|
|
62
64
|
|
|
63
65
|
- **Efficiency**: `Blob` fields are optimized for storing binary data. Buffers are the standard way to handle binary data in Node.js.
|
|
64
66
|
- **Compatibility**: Many HarperDB features and external libraries expect binary data to be in `Buffer` or `Uint8Array` format.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Programmatic Requests with HarperDB Tables
|
|
2
|
+
|
|
3
|
+
In HarperDB, you can interact with your database tables programmatically using the global `tables` object. Each table defined in your schema is available as a property on this object.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
The `tables` object provides a direct way to perform CRUD operations from within your HarperDB resources or scripts.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const track = await tables.ExamplePeople.get(id) as ExamplePerson;
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Available Methods
|
|
14
|
+
|
|
15
|
+
The following methods are available on each table object:
|
|
16
|
+
|
|
17
|
+
### `get(idOrQuery)`
|
|
18
|
+
|
|
19
|
+
Retrieves a single record by ID or multiple records matching a query.
|
|
20
|
+
|
|
21
|
+
- **By ID**:
|
|
22
|
+
```typescript
|
|
23
|
+
const person = await tables.ExamplePeople.get('person-123');
|
|
24
|
+
```
|
|
25
|
+
- **By Query**:
|
|
26
|
+
```typescript
|
|
27
|
+
const albums = await tables.ExamplePeople.get({
|
|
28
|
+
conditions: [{ attribute: 'name', value: 'John' }],
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `put(id, record)`
|
|
33
|
+
|
|
34
|
+
Replaces an entire record with the provided data. If the record doesn't exist, it will be created.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
await tables.ExamplePeople.put('person-123', {
|
|
38
|
+
name: 'Michael Jackson',
|
|
39
|
+
tag: 'entertainer',
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `patch(id, partialRecord)`
|
|
44
|
+
|
|
45
|
+
Performs a partial update on a record. Only the specified fields will be updated.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
await tables.ExamplePeople.patch('person-123', {
|
|
49
|
+
tag: 'tragedy',
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `delete(idOrQuery)`
|
|
54
|
+
|
|
55
|
+
Deletes a record by ID or multiple records matching a query.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
await tables.ExamplePeople.delete('person-123');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `post(record)`
|
|
62
|
+
|
|
63
|
+
Creates a new record. Use this when you want the database to auto-generate a primary key.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const newAlbum = await tables.ExamplePeople.post({
|
|
67
|
+
name: 'John Smith',
|
|
68
|
+
tag: 'anonymous',
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `update(id, updates)`
|
|
73
|
+
|
|
74
|
+
The `update` method is a versatile tool for modifying records. While it can perform simple partial updates like `patch`, its primary power lies in its ability to return an **Updatable Record** object for complex or atomic operations.
|
|
75
|
+
|
|
76
|
+
#### Partial Update (like `patch`)
|
|
77
|
+
|
|
78
|
+
When called with both an ID and an update object, it performs a partial update:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
await tables.ExamplePeople.update('person-123', {
|
|
82
|
+
name: 'Updated Name',
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Getting an Updatable Record
|
|
87
|
+
|
|
88
|
+
When called without an update object (only an ID), `update` returns a reference to the record that can be modified directly.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const person = await tables.ExamplePeople.update('person-123');
|
|
92
|
+
person.name = 'New Person Name';
|
|
93
|
+
// Properties can be assigned directly and are tracked
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Atomic Operations
|
|
97
|
+
|
|
98
|
+
Updatable records provide methods for safe, atomic modifications, which are essential for avoiding race conditions during increments or decrements:
|
|
99
|
+
|
|
100
|
+
- `addTo(attribute, value)`: Atomically adds to a numeric value.
|
|
101
|
+
- `subtractFrom(attribute, value)`: Atomically subtracts from a numeric value.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const stats = await tables.Stats.update('daily');
|
|
105
|
+
stats.addTo('viewCount', 1);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Update without an ID or with Context
|
|
109
|
+
|
|
110
|
+
Calling `update()` without an ID can be used when the target is implied by the context (e.g., inside a resource method) or when you want to get an updatable reference to a collection.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Inside a custom resource method
|
|
114
|
+
const record = await this.update();
|
|
115
|
+
record.set('status', 'processed');
|
|
116
|
+
|
|
117
|
+
// Passing specific context (like a transaction)
|
|
118
|
+
const person = await tables.ExamplePeople.update('person-123', null, {
|
|
119
|
+
transaction,
|
|
120
|
+
});
|
|
121
|
+
person.addTo('playCount', 1);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `search(query)`
|
|
125
|
+
|
|
126
|
+
Performs a search based on the provided query criteria. It returns an `AsyncIterable` which is efficient for streaming large result sets.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const results = await tables.ExamplePeople.search({
|
|
130
|
+
conditions: [{ attribute: 'artist', value: 'Michael Jackson' }],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
for await (const person of results) {
|
|
134
|
+
console.log(person.name);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `subscribe(query)`
|
|
139
|
+
|
|
140
|
+
Subscribes to real-time changes in the table. You can provide a query to filter which changes trigger a notification.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const subscription = await tables.ExamplePeople.subscribe({
|
|
144
|
+
conditions: [{ attribute: 'name', value: 'Thriller' }],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
for await (const event of subscription) {
|
|
148
|
+
console.log('Record changed:', event.value);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `publish(id, message)`
|
|
153
|
+
|
|
154
|
+
Publishes a message to a specific record or topic. This triggers any active subscriptions without necessarily persisting data to the table.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
await tables.ExamplePeople.publish('person-123', {
|
|
158
|
+
type: 'REVALIDATE',
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Querying Options
|
|
164
|
+
|
|
165
|
+
Many methods accept a `Query` (or `RequestTarget`) object. Common options include:
|
|
166
|
+
|
|
167
|
+
- `conditions`: Array of filter conditions.
|
|
168
|
+
- `limit`: Number of records to return.
|
|
169
|
+
- `offset`: Number of records to skip.
|
|
170
|
+
- `sort`: Attribute and direction for sorting.
|
|
171
|
+
- `select`: Array of specific attributes to return.
|
|
172
|
+
|
|
173
|
+
Example of a complex query:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const recentAlbums = await tables.ExamplePeople.get({
|
|
177
|
+
conditions: [{
|
|
178
|
+
attribute: 'releaseDate',
|
|
179
|
+
comparator: 'ge',
|
|
180
|
+
value: '2020-01-01',
|
|
181
|
+
}],
|
|
182
|
+
sort: { attribute: 'releaseDate', descending: true },
|
|
183
|
+
limit: 10,
|
|
184
|
+
});
|
|
185
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Serving Web Content with Harper
|
|
2
|
+
|
|
3
|
+
Harper provides two primary ways to include and serve HTTP web content such as HTML, CSS, JavaScript, and React applications. These methods are mutually exclusive.
|
|
4
|
+
|
|
5
|
+
## 1. Using the Static Plugin
|
|
6
|
+
|
|
7
|
+
The `static` plugin is the simplest way to serve static files. It maps a directory on your filesystem to your Harper REST endpoint.
|
|
8
|
+
|
|
9
|
+
### Configuration
|
|
10
|
+
|
|
11
|
+
Add the `static` configuration to your `config.yaml`:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
static:
|
|
15
|
+
files: 'web/*'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Usage
|
|
19
|
+
|
|
20
|
+
1. Create a `web` folder in your project root.
|
|
21
|
+
2. Place your static files (e.g., `index.html`, `styles.css`, `app.js`) inside the `web` folder.
|
|
22
|
+
3. Your files will be accessible from your REST endpoint. For example, if Harper is running on `http://localhost:9926/`, your `index.html` will be available at that address.
|
|
23
|
+
|
|
24
|
+
### Key Characteristics
|
|
25
|
+
|
|
26
|
+
- **Precedence**: Static files are searched first. If a matching file is found, it is served; otherwise, Harper proceeds to check your other resource and table APIs.
|
|
27
|
+
- **No Directory Listings**: Browsing the directory structure via the browser is not supported.
|
|
28
|
+
- **Simple Deployment**: Ideal for pre-built applications or simple static sites.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Using the Vite Plugin
|
|
33
|
+
|
|
34
|
+
The Vite plugin integrates Vite's development server directly into Harper, providing features like Hot Module Replacement (HMR) during development.
|
|
35
|
+
|
|
36
|
+
### Configuration
|
|
37
|
+
|
|
38
|
+
Add the Vite plugin to your `config.yaml`:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
'@harperfast/vite-plugin':
|
|
42
|
+
package: '@harperfast/vite-plugin'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Setup
|
|
46
|
+
|
|
47
|
+
This plugin expects a `vite.config.ts` and an `index.html` file in your project root. It handles rendering your app through Vite during development.
|
|
48
|
+
|
|
49
|
+
### Package Configuration
|
|
50
|
+
|
|
51
|
+
To use the Vite plugin effectively, you should configure your `package.json` with the appropriate scripts and dependencies.
|
|
52
|
+
|
|
53
|
+
#### Scripts
|
|
54
|
+
|
|
55
|
+
Copy these script examples to manage your development and deployment workflows:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
"scripts": {
|
|
59
|
+
"dev": "harper run .",
|
|
60
|
+
"build": "tsc -b && vite build",
|
|
61
|
+
"build-and-deploy": "rm -Rf deploy && npm run build && mkdir deploy && mv web deploy/ && cp -R deploy-template/* deploy/ && dotenv -- npm run deploy-web && rm -Rf deploy",
|
|
62
|
+
"deploy-web": "(cd deploy && harperdb deploy_component . project=web restart=rolling replicated=true)"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Dependencies and Overrides
|
|
67
|
+
|
|
68
|
+
Include the Vite plugin and other related dependencies in your `devDependencies`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install --save-dev vite @harperfast/vite-plugin @vitejs/plugin-react dotenv-cli
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Deploying to Production
|
|
75
|
+
|
|
76
|
+
Vite's HMR server is meant for development, not production. For that, the `build-and-deploy` script above builds the Vite app into a `web` folder, places the static handler, and then deploys that subdirectory as a Harper app to production.
|
|
77
|
+
|
|
78
|
+
### Key Characteristics
|
|
79
|
+
|
|
80
|
+
- **Development Experience**: Provides Vite's HMR for a fast development cycle.
|
|
81
|
+
- **Integrated Rendering**: Automatically handles the rendering of your React/Vite app.
|
|
82
|
+
- **Mutual Exclusivity**: Use this approach **instead of** the static plugin if you want Vite integration.
|