-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Enhance template with Dog & Owner tables, relationships, and custom logic #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
| 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing if you spell out too that you can
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just do
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!' }; | ||
| } | ||
| } | ||
|
|
||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we recommend splitting out from
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we recommend using |
||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⛏️ Thoughts on changing it to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thoughts on
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
There was a problem hiding this comment.
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?