-
-
Notifications
You must be signed in to change notification settings - Fork 11.9k
feat(skills): add harper-best-practices skill and rules #640
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
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 |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| --- | ||
| name: adding-tables-with-schemas | ||
| description: Guidelines for adding tables to a Harper database using GraphQL schemas. | ||
| --- | ||
|
|
||
| # Adding Tables with Schemas | ||
|
|
||
| Instructions for the agent to follow when adding tables to a Harper database. | ||
|
|
||
| ## When to Use | ||
|
|
||
| Use this skill when you need to define new data structures or modify existing ones in a Harper database. | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. **Create Dedicated Schema Files**: Prefer having a dedicated schema `.graphql` file for each table. Check the `config.yaml` file under `graphqlSchema.files` to see how it's configured. It typically accepts wildcards (e.g., `schemas/*.graphql`), but may be configured to point at a single file. | ||
| 2. **Use Directives**: All available directives for defining your schema are defined in `node_modules/harperdb/schema.graphql`. Common directives include `@table`, `@export`, `@primaryKey`, `@indexed`, and `@relationship`. | ||
| 3. **Define Relationships**: Link tables together using the `@relationship` directive. For more details, see the [Defining Relationships](defining-relationships.md) skill. | ||
| 4. **Enable Automatic APIs**: If you add `@table @export` to a schema type, Harper automatically sets up REST and WebSocket APIs for basic CRUD operations against that table. For a detailed list of available endpoints and how to use them, see the [Automatic REST APIs](automatic-apis.md) skill. | ||
| - `GET /{TableName}`: Describes the schema itself. | ||
| - `GET /{TableName}/`: Lists all records (supports filtering, sorting, and pagination via query parameters). See the [Querying REST APIs](querying-rest-apis.md) skill for details. | ||
| - `GET /{TableName}/{id}`: Retrieves a single record by its ID. | ||
| - `POST /{TableName}/`: Creates a new record. | ||
| - `PUT /{TableName}/{id}`: Updates an existing record. | ||
| - `PATCH /{TableName}/{id}`: Performs a partial update on a record. | ||
| - `DELETE /{TableName}/`: Deletes all records or filtered records. | ||
| - `DELETE /{TableName}/{id}`: Deletes a single record by its ID. | ||
| 5. **Consider Table Extensions**: If you are going to [extend the table](./extending-tables.md) in your resources, then do not `@export` the table from the schema. | ||
|
|
||
| ## Examples | ||
|
|
||
| In a hypothetical `schemas/ExamplePerson.graphql`: | ||
|
|
||
| ```graphql | ||
| type ExamplePerson @table @export { | ||
| id: ID @primaryKey | ||
| name: String | ||
| tag: String @indexed | ||
| } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| --- | ||
| name: automatic-apis | ||
| description: How to use Harper's automatically generated REST and WebSocket APIs. | ||
| --- | ||
|
|
||
| # Automatic APIs | ||
|
|
||
| Instructions for the agent to follow when utilizing Harper's automatic APIs. | ||
|
|
||
| ## When to Use | ||
|
|
||
| Use this skill when you want to interact with Harper tables via REST or WebSockets without writing custom resource logic. This is ideal for basic CRUD operations and real-time updates. | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. **Enable Automatic APIs**: Ensure your GraphQL schema includes the `@export` directive for the table. | ||
| 2. **Access REST Endpoints**: Use the standard endpoints for your table (Note: Paths are case-sensitive). | ||
| 3. **Use Automatic WebSockets**: Connect to `wss://your-harper-instance/{TableName}` to receive events whenever updates are made to that table. This is the easiest way to add real-time capabilities. (Use `ws://` for local development without SSL). For more complex needs, see [Real-time Apps](real-time-apps.md). | ||
| 4. **Apply Filtering and Querying**: Use query parameters with `GET /{TableName}/` and `DELETE /{TableName}/`. See the [Querying REST APIs](querying-rest-apis.md) skill for advanced details. | ||
| 5. **Customize if Needed**: If the automatic APIs don't meet your requirements, [customize the resources](./custom-resources.md). | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Schema Configuration | ||
|
|
||
| ```graphql | ||
| type MyTable @table @export { | ||
| id: ID @primaryKey | ||
| name: String | ||
| } | ||
| ``` | ||
|
|
||
| ### Common REST Operations | ||
|
|
||
| - **List Records**: `GET /MyTable/` | ||
| - **Create Record**: `POST /MyTable/` | ||
| - **Update Record**: `PATCH /MyTable/{id}` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| --- | ||
| name: caching | ||
| description: How to implement integrated data caching in Harper from external sources. | ||
| --- | ||
|
|
||
| # Caching | ||
|
|
||
| Instructions for the agent to follow when implementing caching in Harper. | ||
|
|
||
| ## When to Use | ||
|
|
||
| Use this skill when you need high-performance, low-latency storage for data from external sources. It's ideal for reducing API calls to third-party services, preventing cache stampedes, and making external data queryable as if it were native Harper tables. | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. **Configure a Cache Table**: Define a table in your `schema.graphql` with an `expiration` (in seconds). | ||
| 2. **Define an External Source**: Create a Resource class that fetches the data from your source. | ||
| 3. **Attach Source to Table**: Use `sourcedFrom` to link your resource to the table. | ||
| 4. **Implement Active Caching (Optional)**: Use `subscribe()` for proactive updates. See [Real-Time Apps](real-time-apps.md). | ||
| 5. **Implement Write-Through Caching (Optional)**: Define `put` or `post` in your resource to propagate updates upstream. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Schema Configuration | ||
|
|
||
| ```graphql | ||
| type MyCache @table(expiration: 3600) @export { | ||
| id: ID @primaryKey | ||
| } | ||
| ``` | ||
|
|
||
| ### Resource Implementation | ||
|
|
||
| ```js | ||
| import { Resource, tables } from 'harperdb'; | ||
|
|
||
| export class ThirdPartyAPI extends Resource { | ||
| async get() { | ||
| const id = this.getId(); | ||
| const response = await fetch(`https://api.example.com/items/${id}`); | ||
| if (!response.ok) { | ||
| throw new Error('Source fetch failed'); | ||
| } | ||
| return await response.json(); | ||
| } | ||
| } | ||
|
|
||
| // Attach source to table | ||
| tables.MyCache.sourcedFrom(ThirdPartyAPI); | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| --- | ||
| name: checking-authentication | ||
| description: How to handle user authentication and sessions in Harper Resources. | ||
| --- | ||
|
|
||
| # Checking Authentication | ||
|
|
||
| Instructions for the agent to follow when handling authentication and sessions. | ||
|
|
||
| ## When to Use | ||
|
|
||
| Use this skill when you need to implement sign-in/sign-out functionality, protect specific resource endpoints, or identify the currently logged-in user in a Harper application. | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. **Configure Harper for Sessions**: Ensure `harperdb-config.yaml` has sessions enabled and local auto-authorization disabled for testing: | ||
| ```yaml | ||
| authentication: | ||
| authorizeLocal: false | ||
| enableSessions: true | ||
| ``` | ||
| 2. **Implement Sign In**: Use `this.getContext().login(username, password)` to create a session: | ||
| ```typescript | ||
| async post(_target, data) { | ||
| const context = this.getContext(); | ||
| try { | ||
| await context.login(data.username, data.password); | ||
| } catch { | ||
| return new Response('Invalid credentials', { status: 403 }); | ||
| } | ||
| return new Response('Logged in', { status: 200 }); | ||
| } | ||
| ``` | ||
| 3. **Identify Current User**: Use `this.getCurrentUser()` to access session data: | ||
| ```typescript | ||
| async get() { | ||
| const user = this.getCurrentUser?.(); | ||
| if (!user) return new Response(null, { status: 401 }); | ||
| return { username: user.username, role: user.role }; | ||
| } | ||
| ``` | ||
| 4. **Implement Sign Out**: Use `this.getContext().logout()` or delete the session from context: | ||
| ```typescript | ||
| async post() { | ||
| const context = this.getContext(); | ||
| await context.session?.delete?.(context.session.id); | ||
cubic-dev-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return new Response('Logged out', { status: 200 }); | ||
| } | ||
| ``` | ||
| 5. **Protect Routes**: In your Resource, use `allowRead()`, `allowUpdate()`, etc., to enforce authorization logic based on `this.getCurrentUser()`. For privileged actions, verify `user.role.permission.super_user`. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Sign In Implementation | ||
|
|
||
| ```typescript | ||
| async post(_target, data) { | ||
| const context = this.getContext(); | ||
| try { | ||
| await context.login(data.username, data.password); | ||
| } catch { | ||
| return new Response('Invalid credentials', { status: 403 }); | ||
| } | ||
| return new Response('Logged in', { status: 200 }); | ||
| } | ||
| ``` | ||
|
|
||
| ### Identify Current User | ||
|
|
||
| ```typescript | ||
| async get() { | ||
| const user = this.getCurrentUser?.(); | ||
| if (!user) return new Response(null, { status: 401 }); | ||
| return { username: user.username, role: user.role }; | ||
| } | ||
| ``` | ||
|
|
||
| ### Sign Out Implementation | ||
|
|
||
| ```typescript | ||
| async post() { | ||
| const context = this.getContext(); | ||
| await context.session?.delete?.(context.session.id); | ||
| return new Response('Logged out', { status: 200 }); | ||
| } | ||
| ``` | ||
|
|
||
| ## Status code conventions used here | ||
|
|
||
| - 200: Successful operation. For `GET /me`, a `200` with empty body means “not signed in”. | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - 400: Missing required fields (e.g., username/password on sign-in). | ||
| - 401: No current session for an action that requires one (e.g., sign out when not signed in). | ||
| - 403: Authenticated but not authorized (bad credentials on login attempt, or insufficient privileges). | ||
|
|
||
| ## Client considerations | ||
|
|
||
| - Sessions are cookie-based; the server handles setting and reading the cookie via Harper. If you make cross-origin requests, ensure the appropriate `credentials` mode and CORS settings. | ||
| - If developing locally, double-check the server config still has `authentication.authorizeLocal: false` to avoid accidental superuser bypass. | ||
|
|
||
| ## Token-based auth (JWT + refresh token) for non-browser clients | ||
|
|
||
| Cookie-backed sessions are great for browser flows. For CLI tools, mobile apps, or other non-browser clients, it’s often easier to use **explicit tokens**: | ||
|
|
||
| - **JWT (`operation_token`)**: short-lived bearer token used to authorize API requests. | ||
| - **Refresh token (`refresh_token`)**: longer-lived token used to mint a new JWT when it expires. | ||
|
|
||
| This project includes two Resource patterns for that flow: | ||
|
|
||
| ### Issuing tokens: `IssueTokens` | ||
|
|
||
| **Description / use case:** Generate `{ refreshToken, jwt }` either: | ||
|
|
||
| - with an existing Authorization token (either Basic Auth or a JWT) and you want to issue new tokens, or | ||
| - from an explicit `{ username, password }` payload (useful for direct “login” from a CLI/mobile client). | ||
|
|
||
| ```javascript | ||
| export class IssueTokens extends Resource { | ||
| static loadAsInstance = false; | ||
|
|
||
| async get(target) { | ||
| const { refresh_token: refreshToken, operation_token: jwt } = | ||
| await databases.system.hdb_user.operation( | ||
| { operation: 'create_authentication_tokens' }, | ||
| this.getContext(), | ||
| ); | ||
| return { refreshToken, jwt }; | ||
| } | ||
|
|
||
| async post(target, data) { | ||
| if (!data.username || !data.password) { | ||
| throw new Error('username and password are required'); | ||
| } | ||
|
|
||
| const { refresh_token: refreshToken, operation_token: jwt } = | ||
| await databases.system.hdb_user.operation({ | ||
| operation: 'create_authentication_tokens', | ||
| username: data.username, | ||
| password: data.password, | ||
| }); | ||
| return { refreshToken, jwt }; | ||
| } | ||
| } | ||
| ``` | ||
|
Comment on lines
+116
to
+143
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.
The If import { Resource, databases } from 'harperdb'; // if databases needs an importThe same pattern applies to the |
||
|
|
||
| **Recommended documentation notes to include:** | ||
|
|
||
| - `GET` variant: intended for “I already have an Authorization token, give me new tokens”. | ||
| - `POST` variant: intended for “I have credentials, give me tokens”. | ||
| - Response shape: | ||
| - `refreshToken`: store securely (long-lived). | ||
| - `jwt`: attach to requests (short-lived). | ||
|
|
||
| ### Refreshing a JWT: `RefreshJWT` | ||
|
|
||
| **Description / use case:** When the JWT expires, the client uses the refresh token to get a new JWT without re-supplying username/password. | ||
|
|
||
| ```javascript | ||
| export class RefreshJWT extends Resource { | ||
| static loadAsInstance = false; | ||
|
|
||
| async post(target, data) { | ||
| if (!data.refreshToken) { | ||
| throw new Error('refreshToken is required'); | ||
| } | ||
|
|
||
| const { operation_token: jwt } = await databases.system.hdb_user.operation({ | ||
| operation: 'refresh_operation_token', | ||
| refresh_token: data.refreshToken, | ||
| }); | ||
| return { jwt }; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| **Recommended documentation notes to include:** | ||
|
|
||
| - Requires `refreshToken` in the request body. | ||
| - Returns a new `{ jwt }`. | ||
| - If refresh fails (expired/revoked), client must re-authenticate (e.g., call `IssueTokens.post` again). | ||
|
|
||
| ### Suggested client flow (high-level) | ||
|
|
||
| 1. **Sign in (token flow)** | ||
| - POST /IssueTokens/ with a body of `{ "username": "your username", "password": "your password" }` or GET /IssueTokens/ with an existing Authorization token. | ||
| - Receive `{ jwt, refreshToken }` in the response | ||
| 2. **Call protected APIs** | ||
| - Send the JWT with each request in the Authorization header (as your auth mechanism expects) | ||
| 3. **JWT expires** | ||
| - POST /RefreshJWT/ with a body of `{ "refreshToken": "your refresh token" }`. | ||
| - Receive `{ jwt }` in the response and continue | ||
|
|
||
| ## Quick checklist | ||
|
|
||
| - [ ] Public endpoints explicitly `allowRead`/`allowCreate` as needed. | ||
| - [ ] Sign-in uses `context.login` and handles 400/403 correctly. | ||
| - [ ] Protected routes call `ensureSuperUser(this.getCurrentUser())` (or another role check) before doing work. | ||
| - [ ] Sign-out verifies a session and deletes it. | ||
| - [ ] `authentication.authorizeLocal` is `false` and `enableSessions` is `true` in Harper config. | ||
| - [ ] If using tokens: `IssueTokens` issues `{ jwt, refreshToken }`, `RefreshJWT` refreshes `{ jwt }` with a `refreshToken`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| --- | ||
| name: creating-a-fabric-account-and-cluster | ||
| description: How to create a Harper Fabric account, organization, and cluster. | ||
| --- | ||
|
|
||
| # Creating a Harper Fabric Account and Cluster | ||
|
|
||
| Follow these steps to set up your Harper Fabric environment for deployment. | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. **Sign Up/In**: Go to [https://fabric.harper.fast/](https://fabric.harper.fast/) and sign up or sign in. | ||
| 2. **Create an Organization**: Create an organization (org) to manage your projects. | ||
| 3. **Create a Cluster**: Create a new cluster. This can be on the free tier, no credit card required. | ||
| 4. **Set Credentials**: During setup, set the cluster username and password to finish configuring it. | ||
| 5. **Get Application URL**: Navigate to the **Config** tab and copy the **Application URL**. | ||
| 6. **Configure Environment**: Update your `.env` file or GitHub Actions secrets with cluster-specific credentials. | ||
| 7. **Next Steps**: See the [deploying-to-harper-fabric](deploying-to-harper-fabric.md) rule for detailed instructions on deploying your application successfully. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Environment Configuration | ||
|
|
||
| ```bash | ||
| CLI_TARGET_USERNAME='YOUR_CLUSTER_USERNAME' | ||
| CLI_TARGET_PASSWORD='YOUR_CLUSTER_PASSWORD' | ||
| CLI_TARGET='YOUR_CLUSTER_URL' | ||
| ``` |
Uh oh!
There was an error while loading. Please reload this page.
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.
P2:
GET /meexample returns 401 for anonymous users, but the status-code conventions section specifies a 200 empty-body response for unauthenticated users, creating inconsistent guidance.Prompt for AI agents