Harper Application Template
-
- This is an example of serving static assets using the built-in
- static component.
- You can use this component to serve web applications!
-
0
- - -diff --git a/README.md b/README.md index 9198641..9f951b7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ # Your New Harper Fabric App -This is a template for building [Harper](https://www.harper.fast/) applications. You can download this repository as a starting point for building applications with Harper. +This repository is a template for building applications with [Harper](https://www.harper.fast/). This template includes: ++ 2 tables: Owner, Dog ++ 1 computed field: owner_dog_count ++ 1 relationship: Owners → Dogs ++ 1 custom resource: /OwnerHasBreed + +This template is designed to help you quickly learn Harper’s application model and best practices. + +**Explore the project:** +- `config.yaml`: Main application configuration (routes, settings, etc.) +- `schema.graphql`: Defines tables, fields, relationships, and computed attributes. +- `resources.js`: Implements custom resource classes and endpoint logic. +- `web/`: Contains the frontend web application (HTML, JS, CSS). + +For more information about getting started with HarperDB and building applications, see our [getting started guide](https://docs.harperdb.io/docs). ## Installation @@ -17,29 +31,83 @@ Then you can start your app: npm run dev ``` -Test your application works by querying the `/Greeting` endpoint: +Harper will start at: -```sh -curl http://localhost:9926/Greeting +``` +http://localhost:9926/ ``` -You should see the following: +All tables and resources in this project are automatically available when the app starts. Visit http://localhost:9926/ in your browser to explore the web application and interact with the REST API endpoints. -```json -{"greeting":"Hello, world!"} +## Project Structure + +```graphql +. +├── config.yaml # App configuration +├── schema.graphql # Table definitions, computed fields, relationships +├── resources.js # Custom logic for tables + resources +├── web/ # Client-side web app +│ ├── index.html +│ ├── index.js +│ └── styles.css +└── README.md ``` -Navigate to [http://localhost:9926](http://localhost:9926) in a browser and view the functional web application. +## Database Schema -For more information about getting started with HarperDB and building applications, see our [getting started guide](https://docs.harperdb.io/docs). +All tables are defined in schema.graphql. This template includes two tables: Owner and Dog. Harper automatically exposes REST endpoints for both tables: -For more information on Harper Components, see the [Components documentation](https://docs.harperdb.io/docs/reference/components). +```python-repl +GET /Dog +POST /Dog +PUT /Dog +PATCH /Dog +DELETE /Dog +GET /Owner +... etc. +``` -Take a look at the [default configuration](./config.yaml), which specifies how files are handled in your application. +## Example Workflow -The [schema.graphql](./schema.graphql) is the table schema definition. This is the main starting point for defining your database schema, specifying which tables you want and what attributes/fields they should have. +### 1. Create Dog -The [resources.js](./resources.js) provides a template for defining JavaScript resource classes, for customized application logic in your endpoints. +```sh +curl -X POST http://localhost:9926/Dog \ + -H "Content-Type: application/json" \ + -d '{ + "id": "123", + "name": "Willow", + "breed": "Great Pyrenees" + }' +``` + +### 2. Create Owner + +```sh +curl -X POST http://localhost:9926/Owner \ + -H "Content-Type: application/json" \ + -d '{ + "id": "456", + "name": "Bailey", + "dogIds": ["123"] + }' +``` + +### 3. Use the Custom Resource + +```sh +curl "http://localhost:9926/OwnerHasBreed?ownerName=Bailey&breed=Great%20Pyrenees" +``` + +Response: +```json +{ + "statusCode": 200, + "ownerName": "Bailey", + "breed": "Great Pyrenees", + "hasBreed": true +} +``` ## Deployment diff --git a/resources.js b/resources.js index d51dd3e..876129b 100644 --- a/resources.js +++ b/resources.js @@ -1,24 +1,118 @@ -/** Here we can define any JavaScript-based resources and extensions to tables - -export class MyCustomResource extends tables.TableName { - // we can define our own custom POST handler - post(content) { - // do something with the incoming content; - return super.post(content); - } - // or custom GET handler - get() { - // we can modify this resource before returning - return super.get(); - } +import { tables, Resource } from 'harperdb'; + +const OwnerTable = tables.Owner + +// Computed field example +// +// This shows how to implement a JS-backed computed attribute for a table. +// In schema.graphql, Owner has: +// +// owner_dog_count: Int @computed(version: 1) +// +// The JS implementation below runs whenever Owner records are read or written. +// Computed values are not stored; they are generated on demand by Harper. + +OwnerTable.setComputedAttribute('owner_dog_count', (owner) => { + if (!owner || !Array.isArray(owner.dogIds)) { + return 0; + } + return owner.dogIds.length; +}); + +// Example: extending a table +// +// When you want custom behavior on a table-level REST endpoint, you extend +// the table class produced by Harper's table definition. This allows you to +// override GET, POST, PUT, DELETE, etc. while still inheriting all built-in +// behavior from Harper's table implementation. + +export class OwnerResource extends OwnerTable { + static loadAsInstance = false; + + async post(target, data) { + const record = { ...data }; + // Validate all required fields + const missingFields = []; + if (!record.id) missingFields.push('id'); + if (!record.name) missingFields.push('name'); + if (!Array.isArray(record.dogIds)) missingFields.push('dogIds (must be array)'); + + if (missingFields.length > 0) { + return { + status: 400, + message: `Missing or invalid fields: ${missingFields.join(', ')}`, + }; + } + + const created = await OwnerTable.create(record, this); + return created; + } +} + + +// Example: custom resource using relationships +// +// This resource is not tied to a specific table. It demonstrates how to: +// +// 1. Accept query parameters (?ownerName=...&breed=...) +// 2. Query a table using a relationship-aware select structure +// 3. Iterate through owners and their related dogs (Owner → Dogs relationship) +// 4. Return a boolean indicating whether the owner has any dog of the +// specified breed + +export class OwnerHasBreed extends Resource { + static loadAsInstance = false; + + async get(target) { + const { ownerName, breed } = target.get('ownerName') && target.get('breed') + ? { ownerName: target.get('ownerName'), breed: target.get('breed') } + : {}; + + if (!ownerName || !breed) { + console.warn('[OwnerHasBreed] Missing required query parameters:', { ownerName, breed }); + return { + statusCode: 400, + message: "Missing required query parameters: ownerName and breed", + }; + } + + const query = { + select: [ + "id", + "name", + "dogIds", + { + name: "dogs", + select: ["id", "name", "breed"], + }, + ], + conditions: [{ attribute: "name", comparator: "eq", value: ownerName }], + limit: 1, + }; + + let owner = null; + for await (const o of OwnerTable.search(query)) { + owner = o; + } + + // No owner found → 404 + if (!owner) { + console.warn('[OwnerHasBreed] No owner found for name:', ownerName); + return { + statusCode: 404, + message: `No owner found with name "${ownerName}"`, + }; + } + + const dogs = Array.isArray(owner.dogs) ? owner.dogs : []; + const hasBreed = dogs.some((dog) => dog && dog.breed === breed); + + return { + statusCode: 200, + ownerName, + breed, + hasBreed, + }; + } } - */ -// we can also define a custom resource without a specific table -export class Greeting extends Resource { - // a "Hello, world!" handler - static loadAsInstance = false; // use the updated/newer Resource API - - get() { - return { greeting: 'Hello, world!' }; - } -} \ No newline at end of file + diff --git a/schema.graphql b/schema.graphql index 5ad5d48..8c6ad68 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,7 +1,24 @@ ## Here we can define any tables in our database. This example shows how we define a type as a table using ## the type name as the table name and specifying it is an "export" available in the REST and other external protocols. -type TableName @table @export { - id: ID @primaryKey # Here we define primary key (must be one) - name: String # we can define any other attributes here - tag: String @indexed # we can specify any attributes that should be indexed +## This example uses Owners and Dogs, with a relationship and a computed value. + +type Owner @table(database: "application") @export { + id: ID @primaryKey # Here we define primary key (must be one) + name: String @indexed # we can specify any attributes that should be indexed + dogIds: [ID] @indexed + + # Computed: how many dogs this owner has, based on dogIds. + owner_dog_count: Int @computed(version: 1) + + # Relationship: resolve dogIds into Dog records. + dogs: [Dog] @relationship(from: dogIds) +} + +type Dog @table(database: "application") @export { + id: ID @primaryKey + name: String! @indexed + breed: String! @indexed + + # Relationship back to Owner using the dogIds on Owner. + owner: [Owner] @relationship(to: dogIds) } diff --git a/web/hdb-logo.png b/web/hdb-logo.png new file mode 100644 index 0000000..4ec9611 Binary files /dev/null and b/web/hdb-logo.png differ diff --git a/web/index.html b/web/index.html index f801ee9..475c51f 100644 --- a/web/index.html +++ b/web/index.html @@ -3,26 +3,115 @@
-
- This is an example of serving static assets using the built-in
- static component.
- You can use this component to serve web applications!
-
0
- - -
+ Interact with your Harper endpoints from the browser.
+Send requests to /Owner or /Dog.
+ Sends GET /OwnerHasBreed?ownerName=...&breed=....
+
Shows the last JSON response from any request.
+// Ready. Submit a request above.+