Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 81 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
140 changes: 117 additions & 23 deletions resources.js
Original file line number Diff line number Diff line change
@@ -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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to be encouraging users to roll their own validation rather than using built-in validation?

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing if you spell out too that you can GET /OwnerHasBreed to hit this, and that it will be case-sensitive, right? That should help avoid confusion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did notice you highlighting it in the README, but adding a reference here too, even as simple as GET /OwnerHasBreed, should cut down on confusion.

//
// 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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just do OwnerTable.get(ownerName) instead of this whole query?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I probably should switch it. I wanted to have some example in here where we are showing how to query, especially with nested relationships

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!' };
}
}

25 changes: 21 additions & 4 deletions schema.graphql
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we recommend splitting out from data? I have a vague recollection of sticking with data being the recommendation, unless someone is starting to really branch out into multiple apps 🤷 I don't have strong feelings on the topic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we recommend using data for the application; one less thing to think about (unless, like Dawson said, there are multiple applications being independently).

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Thoughts on changing it to ownerDogCount: Int @computed(version: 1), and any references? For consistency.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah good call i didn't even realize I was using both snake and camel haha


# Relationship: resolve dogIds into Dog records.
dogs: [Dog] @relationship(from: dogIds)
}

type Dog @table(database: "application") @export {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on @sealeding this too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good call

id: ID @primaryKey
name: String! @indexed
breed: String! @indexed

# Relationship back to Owner using the dogIds on Owner.
owner: [Owner] @relationship(to: dogIds)
}
Binary file added web/hdb-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading