@harperfast/template-react-ts-studio 1.12.3 → 1.12.4

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.
@@ -18,14 +18,14 @@ Use this skill when you need to define new data structures or modify existing on
18
18
  2. **Use Directives**: All available directives for defining your schema are defined in `node_modules/harper/schema.graphql`. Common directives include `@table`, `@export`, `@primaryKey`, `@indexed`, and `@relationship`.
19
19
  3. **Define Relationships**: Link tables together using the `@relationship` directive. For more details, see the [Defining Relationships](defining-relationships.md) skill.
20
20
  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. **Important**: REST endpoints also require `rest: true` in `config.yaml` — without it, `@export`ed tables will not respond to HTTP requests. For a detailed list of available endpoints and how to use them, see the [Automatic REST APIs](automatic-apis.md) skill.
21
- - `GET /{TableName}`: Describes the schema itself.
21
+ - `GET /{TableName}`: Describes the resource itself — table, database, and declared attributes. No trailing slash.
22
22
  - `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.
23
- - `GET /{TableName}/{id}`: Retrieves a single record by its ID.
24
- - `POST /{TableName}/`: Creates a new record.
25
- - `PUT /{TableName}/{id}`: Updates an existing record.
26
- - `PATCH /{TableName}/{id}`: Performs a partial update on a record.
27
- - `DELETE /{TableName}/`: Deletes all records or filtered records.
28
- - `DELETE /{TableName}/{id}`: Deletes a single record by its ID.
23
+ - `GET /{TableName}/{id}`: Retrieves a single record by its primary key.
24
+ - `POST /{TableName}/`: Creates a record and returns `201` with the Harper-assigned primary key in the `Location` response header (the bare key, not a URL). **The trailing slash is required** — `POST /{TableName}` returns `404`, and `POST /{TableName}/{id}` returns `405`.
25
+ - `PUT /{TableName}/{id}`: Creates **or replaces** the record at `{id}` (upsert). **Properties omitted from the body are removed** — send the complete record, or use `PATCH` to change a subset. Three exceptions survive the replacement: an `@updatedTime` attribute is re-stamped with the time of the write, a `@createdTime` attribute keeps its original value, and the primary key is forced to match the `{id}` in the URL, so a mismatched key in the body cannot create a second record.
26
+ - `PATCH /{TableName}/{id}`: Merges the request body into the existing record, preserving unspecified properties. The merge is **shallow** — a nested object in the body replaces the stored one wholesale rather than being deep-merged.
27
+ - `DELETE /{TableName}/{id}`: Deletes a single record by its primary key.
28
+ - `DELETE /{TableName}/`: Deletes every record matching the query parameters. **With no query parameters it matches — and deletes — every record in the table.** Always pass a filter unless emptying the table is the intent.
29
29
  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.
30
30
 
31
31
  #### Examples
@@ -1241,207 +1241,100 @@ Instructions for the agent to handle user authentication, sessions, and JWT toke
1241
1241
 
1242
1242
  #### When to Use
1243
1243
 
1244
- Apply this rule when implementing login/logout flows, protecting Resource endpoints by checking the current user, issuing or refreshing JWT tokens, or configuring token expiry in Harper. Use it whenever a custom Resource needs to authenticate callers or mint credentials for downstream consumers. See [custom-resources.md](custom-resources.md) for the broader Resource authoring context.
1244
+ Apply this rule when implementing login/logout flows, reading the current authenticated user, issuing or refreshing JWT tokens, or minting scoped tokens from a custom Resource. Use it whenever a Resource must gate behavior on identity or credentials. See [custom-resources.md](custom-resources.md) for the broader Resource authoring model.
1245
1245
 
1246
1246
  #### How It Works
1247
1247
 
1248
- 1. **Check the current authenticated user**: Call `getCurrentUser()` inside any Resource method. It returns the user object (with `username`, `role`, and `role.permission`) or `undefined` if unauthenticated. Guard endpoints by returning a 401 when no user is present.
1248
+ ##### 1. Reading the Current Authenticated User
1249
1249
 
1250
- ```javascript
1251
- async get(target) {
1252
- const user = this.getCurrentUser();
1253
- if (!user) return new Response(null, { status: 401 });
1254
- return { username: user.username, role: user.role };
1255
- }
1256
- ```
1257
-
1258
- 2. **Enable sessions before using login/logout**: Set `authentication.enableSessions: true` in `harperdb-config.yaml`. Without this, `context.login` and `context.session` are unavailable.
1259
-
1260
- 3. **Implement login via `getContext()`**: Call `this.getContext()` to obtain the request context, then call `context.login(username, password)` to verify credentials and establish a session cookie.
1261
-
1262
- ```javascript
1263
- export class SignIn extends Resource {
1264
- async post(_target, data) {
1265
- const context = this.getContext();
1266
- try {
1267
- await context.login(data.username, data.password);
1268
- } catch {
1269
- return new Response('Invalid credentials', { status: 403 });
1270
- }
1271
- return new Response('Logged in', { status: 200 });
1272
- }
1273
- }
1274
- ```
1275
-
1276
- 4. **Implement logout**: Delete the session via `context.session.delete(context.session.id)`.
1277
-
1278
- ```javascript
1279
- export class SignOut extends Resource {
1280
- async post() {
1281
- const context = this.getContext();
1282
- if (!context.session) return new Response(null, { status: 401 });
1283
- await context.session.delete(context.session.id);
1284
- return new Response('Logged out', { status: 200 });
1285
- }
1286
- }
1287
- ```
1250
+ **In an instance method**, call `getCurrentUser()` to get the user associated with the current request, or `undefined` if unauthenticated. The returned object exposes `username`, `role`, and `role.permission` flags.
1288
1251
 
1289
- Cookie-based sessions are intended for browser clients. For non-browser clients, use JWT issuance (steps below).
1252
+ **In a static verb**, read the user from the `context` argument instead — `context.user`:
1290
1253
 
1291
- 5. **Create authentication tokens**: Call `create_authentication_tokens` with credentials. No `Authorization` header is required for this operation.
1254
+ ```javascript
1255
+ static async get(_target, context) {
1256
+ const user = context?.user;
1257
+ if (!user) return new Response(null, { status: 401 });
1258
+ return { username: user.username, role: user.role };
1259
+ }
1260
+ ```
1292
1261
 
1293
- ```json
1294
- {
1295
- "operation": "create_authentication_tokens",
1296
- "username": "username",
1297
- "password": "password"
1298
- }
1299
- ```
1262
+ ##### 2. Reading the Full Context
1300
1263
 
1301
- Response:
1264
+ **In an instance method**, call `getContext()` to retrieve the current context, which includes:
1302
1265
 
1303
- ```json
1304
- {
1305
- "operation_token": "<jwt-operation-token>",
1306
- "refresh_token": "<jwt-refresh-token>"
1307
- }
1308
- ```
1266
+ - `user` — User object with username, role, and authorization information
1267
+ - `transaction` — The current transaction
1309
1268
 
1310
- 6. **Use the operation token**: Pass it as a `Bearer` token in the `Authorization` header on subsequent requests.
1269
+ **In a static verb**, the context is the trailing argument:
1311
1270
 
1312
- ```bash
1313
- curl --location --request POST 'http://localhost:9925' \
1314
- --header 'Content-Type: application/json' \
1315
- --header 'Authorization: Bearer <operation_token>' \
1316
- --data-raw '{
1317
- "operation": "search_by_hash",
1318
- "schema": "dev",
1319
- "table": "dog",
1320
- "hash_values": [1],
1321
- "get_attributes": ["*"]
1322
- }'
1323
- ```
1271
+ - `(target, context)` for `get`/`delete`
1272
+ - `(target, data, context)` for `put`/`patch`/`post`
1324
1273
 
1325
- 7. **Refresh an expired operation token**: When the `operation_token` expires, use `refresh_operation_token` and pass the `refresh_token` as `Bearer <refresh_token>`.
1274
+ When triggered by HTTP, the context also exposes `url`, `method`, `headers`, `responseHeaders`, `pathname`, `host`, `ip`, `body`, `data`, `lastModified`, and `requestContext`.
1326
1275
 
1327
- ```bash
1328
- curl --location --request POST 'http://localhost:9925' \
1329
- --header 'Content-Type: application/json' \
1330
- --header 'Authorization: Bearer <refresh_token>' \
1331
- --data-raw '{
1332
- "operation": "refresh_operation_token"
1333
- }'
1334
- ```
1276
+ ##### 3. Handling Sessions and Login
1335
1277
 
1336
- Response:
1278
+ Enable sessions in `harper-config.yaml`:
1337
1279
 
1338
- ```json
1339
- {
1340
- "operation_token": "<new-jwt-operation-token>"
1341
- }
1342
- ```
1343
-
1344
- When both tokens have expired, call `create_authentication_tokens` again with username and password.
1280
+ ```yaml
1281
+ authentication:
1282
+ enableSessions: true
1283
+ ```
1345
1284
 
1346
- 8. **Mint scoped tokens for limited access**: A super user can embed an inline role in `create_authentication_tokens` using the same `permission` structure as `add_role`. Include `expires_in` to control lifetime. Do not include a `password` field. The `username` is attribution only and must not match an existing user.
1285
+ Use `context.login` in a static `post` verb to verify credentials and establish a session cookie:
1347
1286
 
1348
- ```json
1349
- {
1350
- "operation": "create_authentication_tokens",
1351
- "username": "reporting-service",
1352
- "role": {
1353
- "permission": {
1354
- "operations": ["read_only"],
1355
- "dev": {
1356
- "tables": {
1357
- "dog": {
1358
- "read": true,
1359
- "insert": false,
1360
- "update": false,
1361
- "delete": false,
1362
- "attribute_permissions": []
1363
- }
1364
- }
1365
- }
1366
- }
1367
- },
1368
- "expires_in": "7d"
1369
- }
1370
- ```
1371
-
1372
- Key constraints for scoped tokens:
1373
- - No refresh token is issued; no user record is created.
1374
- - Scoped tokens cannot be revoked before expiry — choose short `expires_in` values.
1375
- - `super_user` and `cluster_user` are always forced to `false` in the embedded role.
1376
- - In mixed-version clusters, only nodes with scoped-token support accept these tokens; older nodes return 401.
1287
+ ```javascript
1288
+ export class SignIn extends Resource {
1289
+ static async post(_target, data, context) {
1290
+ const { username, password } = (await data) ?? {};
1291
+ try {
1292
+ await context.login(username, password);
1293
+ } catch {
1294
+ return new Response('Invalid credentials', { status: 403 });
1295
+ }
1296
+ return new Response('Logged in', { status: 200 });
1297
+ }
1298
+ }
1377
1299
 
1378
- 9. **Issue tokens from a custom Resource using `server.operation`**: Import `server` from `harper` and call `server.operation()` to mint tokens programmatically. Pass `true` as the **third argument** to run the operation as the current authenticated user; omit it when supplying credentials directly.
1300
+ export class SignOut extends Resource {
1301
+ static async post(_target, _data, context) {
1302
+ if (!context?.session?.user) return new Response(null, { status: 401 });
1303
+ await context.session.update({ user: null });
1304
+ return new Response('Logged out', { status: 200 });
1305
+ }
1306
+ }
1307
+ ```
1379
1308
 
1380
- ```javascript
1381
- import { Resource, server } from 'harper';
1382
-
1383
- export class IssueTokens extends Resource {
1384
- static async get(_target, context) {
1385
- const { operation_token, refresh_token } = await server.operation(
1386
- { operation: 'create_authentication_tokens' },
1387
- context,
1388
- true,
1389
- );
1390
- return { operation_token, refresh_token };
1391
- }
1309
+ - `context.login(username, password)` verifies credentials and sets the session cookie on success.
1310
+ - To end a session, call `context.session.update({ user: null })` — `update` is the session's only mutator.
1311
+ - `context.session` is an empty object (not `undefined`) when sessions are enabled and no session cookie is present; test `context?.session?.user` to detect an established session.
1312
+ - Cookie-based sessions are for browser clients. For non-browser clients, use JWT issuance.
1392
1313
 
1393
- static async post(_target, data) {
1394
- const { username, password } = await data;
1395
- if (!username || !password) {
1396
- return new Response('username and password required', { status: 400 });
1397
- }
1398
- const { operation_token, refresh_token } = await server.operation({
1399
- operation: 'create_authentication_tokens',
1400
- username,
1401
- password,
1402
- });
1403
- return { operation_token, refresh_token };
1404
- }
1405
- }
1314
+ ##### 4. Creating Authentication Tokens
1406
1315
 
1407
- export class RefreshJWT extends Resource {
1408
- static async post(_target, data) {
1409
- const { refresh_token } = await data;
1410
- if (!refresh_token) {
1411
- return new Response('refresh_token required', { status: 400 });
1412
- }
1413
- const { operation_token } = await server.operation({
1414
- operation: 'refresh_operation_token',
1415
- refresh_token,
1416
- });
1417
- return { operation_token };
1418
- }
1419
- }
1420
- ```
1316
+ Call `create_authentication_tokens` with credentials in the body — no `Authorization` header is required when `username` and `password` are supplied:
1421
1317
 
1422
- 10. **Configure token expiry**: Set timeouts in `harper-config.yaml` under the `authentication` section. Values follow the `jsonwebtoken` duration string format (e.g., `1d`, `12h`, `60m`).
1318
+ ```json
1319
+ {
1320
+ "operation": "create_authentication_tokens",
1321
+ "username": "username",
1322
+ "password": "password"
1323
+ }
1324
+ ```
1423
1325
 
1424
- ```yaml
1425
- authentication:
1426
- operationTokenTimeout: 1d # Default: 1 day
1427
- refreshTokenTimeout: 30d # Default: 30 days
1428
- ```
1326
+ Response:
1429
1327
 
1430
- #### Examples
1328
+ ```json
1329
+ {
1330
+ "operation_token": "<jwt-operation-token>",
1331
+ "refresh_token": "<jwt-refresh-token>"
1332
+ }
1333
+ ```
1431
1334
 
1432
- ##### Full JWT flow via cURL
1335
+ Pass the `operation_token` as a `Bearer` token on subsequent requests:
1433
1336
 
1434
1337
  ```bash
1435
- # Step 1: Create tokens
1436
- curl --location --request POST 'http://localhost:9925' \
1437
- --header 'Content-Type: application/json' \
1438
- --data-raw '{
1439
- "operation": "create_authentication_tokens",
1440
- "username": "username",
1441
- "password": "password"
1442
- }'
1443
-
1444
- # Step 2: Use operation token
1445
1338
  curl --location --request POST 'http://localhost:9925' \
1446
1339
  --header 'Content-Type: application/json' \
1447
1340
  --header 'Authorization: Bearer <operation_token>' \
@@ -1452,8 +1345,13 @@ curl --location --request POST 'http://localhost:9925' \
1452
1345
  "hash_values": [1],
1453
1346
  "get_attributes": ["*"]
1454
1347
  }'
1348
+ ```
1455
1349
 
1456
- # Step 3: Refresh when operation token expires
1350
+ ##### 5. Refreshing the Operation Token
1351
+
1352
+ When the `operation_token` expires, pass the `refresh_token` as `Bearer <refresh_token>` and call `refresh_operation_token`:
1353
+
1354
+ ```bash
1457
1355
  curl --location --request POST 'http://localhost:9925' \
1458
1356
  --header 'Content-Type: application/json' \
1459
1357
  --header 'Authorization: Bearer <refresh_token>' \
@@ -1462,14 +1360,138 @@ curl --location --request POST 'http://localhost:9925' \
1462
1360
  }'
1463
1361
  ```
1464
1362
 
1465
- ##### Session-based login/logout Resource
1363
+ Response:
1364
+
1365
+ ```json
1366
+ {
1367
+ "operation_token": "<new-jwt-operation-token>"
1368
+ }
1369
+ ```
1370
+
1371
+ When both tokens have expired, call `create_authentication_tokens` again with username and password.
1372
+
1373
+ ##### 6. Minting Scoped Tokens
1374
+
1375
+ A `super_user` can mint a scoped token with embedded permissions using `create_authentication_tokens` with an inline `role`. The minter must be authenticated; no `password` may be included in the body. Use `add_role`-style `permission` structure:
1376
+
1377
+ ```json
1378
+ {
1379
+ "operation": "create_authentication_tokens",
1380
+ "username": "reporting-service",
1381
+ "role": {
1382
+ "permission": {
1383
+ "operations": ["read_only"],
1384
+ "dev": {
1385
+ "tables": {
1386
+ "dog": {
1387
+ "read": true,
1388
+ "insert": false,
1389
+ "update": false,
1390
+ "delete": false,
1391
+ "attribute_permissions": []
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ },
1397
+ "expires_in": "7d"
1398
+ }
1399
+ ```
1400
+
1401
+ Authenticate the mint request with Basic Authentication or an existing `super_user` `operation_token`:
1402
+
1403
+ ```bash
1404
+ curl --location --request POST 'http://localhost:9925' \
1405
+ --header 'Content-Type: application/json' \
1406
+ --header 'Authorization: Basic <base64 of super_user:password>' \
1407
+ --data-raw '{
1408
+ "operation": "create_authentication_tokens",
1409
+ "username": "reporting-service",
1410
+ "role": { "permission": { "operations": ["read_only"] } },
1411
+ "expires_in": "7d"
1412
+ }'
1413
+ ```
1414
+
1415
+ Key constraints for scoped tokens:
1416
+
1417
+ - `username` is attribution only and must not name an existing user.
1418
+ - No refresh token is issued; no user record is created.
1419
+ - Scoped tokens cannot be revoked before expiry — choose `expires_in` carefully and prefer short lifetimes.
1420
+ - `super_user` and `cluster_user` are always forced to `false` in the embedded role.
1421
+ - In **mixed-version** clusters, only nodes with scoped-token support accept these tokens; older nodes reject them with 401.
1422
+
1423
+ ##### 7. Issuing Tokens from a Custom Resource
1424
+
1425
+ Use `server.operation()` to mint tokens programmatically inside a Resource. Pass `authorize: true` as the **third argument** when the operation should run as the current authenticated user:
1426
+
1427
+ ```javascript
1428
+ import { Resource, server } from 'harper';
1429
+
1430
+ export class IssueTokens extends Resource {
1431
+ static async get(_target, context) {
1432
+ // Issue tokens for the current authenticated user
1433
+ const { operation_token, refresh_token } = await server.operation(
1434
+ { operation: 'create_authentication_tokens' },
1435
+ context,
1436
+ true, // third argument: authorize as current user
1437
+ );
1438
+ return { operation_token, refresh_token };
1439
+ }
1440
+
1441
+ static async post(_target, data) {
1442
+ // Issue tokens from credentials supplied in the body
1443
+ const { username, password } = await data;
1444
+ if (!username || !password) {
1445
+ return new Response('username and password required', { status: 400 });
1446
+ }
1447
+ const { operation_token, refresh_token } = await server.operation({
1448
+ operation: 'create_authentication_tokens',
1449
+ username,
1450
+ password,
1451
+ });
1452
+ return { operation_token, refresh_token };
1453
+ }
1454
+ }
1455
+
1456
+ export class RefreshJWT extends Resource {
1457
+ static async post(_target, data) {
1458
+ const { refresh_token } = await data;
1459
+ if (!refresh_token) {
1460
+ return new Response('refresh_token required', { status: 400 });
1461
+ }
1462
+ const { operation_token } = await server.operation({
1463
+ operation: 'refresh_operation_token',
1464
+ refresh_token,
1465
+ });
1466
+ return { operation_token };
1467
+ }
1468
+ }
1469
+ ```
1470
+
1471
+ Omit the third argument (or pass `false`) when the operation supplies its own credentials.
1472
+
1473
+ ##### 8. Configuring Token Expiry
1474
+
1475
+ Set timeouts in `harper-config.yaml` under the `authentication` section:
1476
+
1477
+ ```yaml
1478
+ authentication:
1479
+ operationTokenTimeout: 1d # Default: 1 day
1480
+ refreshTokenTimeout: 30d # Default: 30 days
1481
+ ```
1482
+
1483
+ Valid duration strings follow the `jsonwebtoken` package format (e.g., `1d`, `12h`, `60m`). The `expires_in` field on scoped token minting accepts the same format.
1484
+
1485
+ #### Examples
1486
+
1487
+ ##### Full Sign-In / Sign-Out Resource
1466
1488
 
1467
1489
  ```javascript
1468
1490
  export class SignIn extends Resource {
1469
- async post(_target, data) {
1470
- const context = this.getContext();
1491
+ static async post(_target, data, context) {
1492
+ const { username, password } = (await data) ?? {};
1471
1493
  try {
1472
- await context.login(data.username, data.password);
1494
+ await context.login(username, password);
1473
1495
  } catch {
1474
1496
  return new Response('Invalid credentials', { status: 403 });
1475
1497
  }
@@ -1478,23 +1500,44 @@ export class SignIn extends Resource {
1478
1500
  }
1479
1501
 
1480
1502
  export class SignOut extends Resource {
1481
- async post() {
1482
- const context = this.getContext();
1483
- if (!context.session) return new Response(null, { status: 401 });
1484
- await context.session.delete(context.session.id);
1503
+ static async post(_target, _data, context) {
1504
+ if (!context?.session?.user) return new Response(null, { status: 401 });
1505
+ await context.session.update({ user: null });
1485
1506
  return new Response('Logged out', { status: 200 });
1486
1507
  }
1487
1508
  }
1488
1509
  ```
1489
1510
 
1511
+ ##### Reading the Current Authenticated User in a Static Verb
1512
+
1513
+ ```javascript
1514
+ static async get(_target, context) {
1515
+ const user = context?.user;
1516
+ if (!user) return new Response(null, { status: 401 });
1517
+ return { username: user.username, role: user.role };
1518
+ }
1519
+ ```
1520
+
1521
+ ##### Minting Tokens via cURL
1522
+
1523
+ ```bash
1524
+ curl --location --request POST 'http://localhost:9925' \
1525
+ --header 'Content-Type: application/json' \
1526
+ --data-raw '{
1527
+ "operation": "create_authentication_tokens",
1528
+ "username": "username",
1529
+ "password": "password"
1530
+ }'
1531
+ ```
1532
+
1490
1533
  #### Notes
1491
1534
 
1492
- - JWT authentication is **preferred over Basic Auth** when you want to avoid sending credentials on every request, when the client can store tokens, or when making multiple sequential requests. For simple or server-to-server scenarios, use Basic Authentication.
1535
+ - JWT authentication is **preferred over Basic Auth** when you want to avoid sending credentials on every request, your client can store tokens, or you have multiple sequential requests. For simple or **server-to-server** scenarios, use **Basic Authentication**.
1493
1536
  - Always use **HTTPS** in production to protect tokens in transit.
1494
1537
  - Treat tokens like passwords. If a token is compromised, it remains valid until expiry — use shorter `operationTokenTimeout` values in high-security environments.
1495
- - `enableSessions` must be `true` in config before `context.login` or `context.session` will work.
1496
- - The `third argument` (`true`) to `server.operation` controls whether the operation runs as the current authenticated user. Omit it when the operation body supplies its own credentials.
1497
- - Scoped tokens have a 12KB limit when encoded into an `Authorization` header.
1538
+ - `context.login` and `context.session` require `enableSessions: true` in `harperdb-config.yaml`; they are for browser clients only.
1539
+ - The `server.operation()` third argument (`authorize: true`) attributes the operation to — and permission-checks against — the current authenticated user. Omit it when supplying credentials directly in the operation body.
1540
+ - In **mixed-version** clusters, scoped tokens are only accepted by nodes that support them; older nodes return 401.
1498
1541
 
1499
1542
  ## 3. Logic & Extension
1500
1543
 
@@ -1504,11 +1547,11 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
1504
1547
 
1505
1548
  #### When to Use
1506
1549
 
1507
- Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed schema, or when a specific URL shape is required.
1550
+ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed resource or when a specific URL shape is required.
1508
1551
 
1509
1552
  #### How It Works
1510
1553
 
1511
- 1. **Import `Resource` from `harper`**: Always import from the `harper` package rather than relying on globals.
1554
+ 1. **Import `Resource` from the `harper` package**: Always import explicitly rather than relying on globals.
1512
1555
 
1513
1556
  ```javascript
1514
1557
  import { tables, Resource } from 'harper';
@@ -1526,7 +1569,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1526
1569
  }
1527
1570
  ```
1528
1571
 
1529
- 3. **Use `async` static methods for external calls**: Await fetch or other async operations inside `static` handlers.
1572
+ 3. **Use `async` static methods to call external services**: Return or forward the response directly.
1530
1573
 
1531
1574
  ```javascript
1532
1575
  export class MyExternalData extends Resource {
@@ -1544,7 +1587,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1544
1587
  }
1545
1588
  ```
1546
1589
 
1547
- 4. **Export the class to create an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
1590
+ 4. **Export the class to expose it as an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
1548
1591
 
1549
1592
  | Export form | URL | Notes |
1550
1593
  | ------------------------------------------- | --------------- | --------------------------------------------------------------- |
@@ -1555,12 +1598,17 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1555
1598
  | `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
1556
1599
  | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
1557
1600
 
1558
- URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
1601
+ 5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
1559
1602
 
1560
- 5. **Declare path parameters with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
1603
+ ```javascript
1604
+ server.resources.set('my-path', Foo);
1605
+ ```
1606
+
1607
+ 6. **Declare dynamic path segments with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
1561
1608
 
1562
1609
  ```javascript
1563
1610
  export class Widget extends Resource {
1611
+ // GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
1564
1612
  static path = '/widget/:id/action/:action';
1565
1613
  static get(target) {
1566
1614
  return { id: target.id, action: target.action };
@@ -1568,22 +1616,11 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
1568
1616
  }
1569
1617
  ```
1570
1618
 
1571
- A `static path` takes precedence over the export name. A leading `/` makes the path root-relative (top-level). A leading `./` or bare name resolves relative to the component directory.
1572
-
1573
- 6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` when the path cannot be known at export time.
1574
-
1575
- ```javascript
1576
- server.resources.set('my-path', Foo);
1577
- ```
1578
-
1579
- 7. **Optionally source a table from a custom resource**: Use the resource as a caching layer for a local table.
1580
- ```javascript
1581
- tables.MyCache.sourcedFrom(MyExternalData);
1582
- ```
1619
+ 7. **Resolve path precedence correctly**: Exact and static paths always win over parameterized ones. Among parameterized routes, more specific paths win: a literal segment beats a `:param`, which beats a `*` wildcard, compared left to right.
1583
1620
 
1584
1621
  #### Examples
1585
1622
 
1586
- ##### External API wrapper with GET and PUT
1623
+ **Wrapping an external API and using it as a cache source:**
1587
1624
 
1588
1625
  ```javascript
1589
1626
  import { tables, Resource } from 'harper';
@@ -1606,19 +1643,9 @@ export class MyExternalData extends Resource {
1606
1643
  tables.MyCache.sourcedFrom(MyExternalData);
1607
1644
  ```
1608
1645
 
1609
- ##### Path parameters with `static path`
1646
+ **Catch-all wildcard path:**
1610
1647
 
1611
1648
  ```javascript
1612
- import { Resource } from 'harper';
1613
-
1614
- export class Widget extends Resource {
1615
- // GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
1616
- static path = '/widget/:id/action/:action';
1617
- static get(target) {
1618
- return { id: target.id, action: target.action };
1619
- }
1620
- }
1621
-
1622
1649
  export class Files extends Resource {
1623
1650
  // GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
1624
1651
  static path = '/files/*rest';
@@ -1628,28 +1655,27 @@ export class Files extends Resource {
1628
1655
  }
1629
1656
  ```
1630
1657
 
1631
- ##### Programmatic registration
1658
+ **Root-relative fixed route:**
1632
1659
 
1633
1660
  ```javascript
1634
- import { Resource } from 'harper';
1635
-
1636
- export class Foo extends Resource {
1661
+ export class AcmeChallenge extends Resource {
1662
+ static path = '/.well-known/acme-challenge/:token';
1637
1663
  static get(target) {
1638
- return { data: doSomething() };
1664
+ return { token: target.token };
1639
1665
  }
1640
1666
  }
1641
-
1642
- server.resources.set('my-path', Foo);
1643
1667
  ```
1644
1668
 
1645
1669
  #### Notes
1646
1670
 
1647
- - A bare `*` wildcard (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
1648
- - Resolution order: exact/static paths always win over parameterized ones. Among parameterized routes, more specific paths win — a literal segment beats `:param`, which beats `*`, compared left to right.
1649
- - Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as `{param}` URI templates.
1650
- - If a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
1671
+ - URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
1672
+ - A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
1673
+ - A leading `./` or a bare name in `static path` resolves relative to the component directory.
1674
+ - A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
1675
+ - `static path` takes precedence over the export name when both are present.
1676
+ - Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as URI templates.
1677
+ - When a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
1651
1678
  - Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
1652
- - Harper runs as a single process — `tables`, `databases`, and other APIs are the same live, process-wide objects regardless of which component accesses them.
1653
1679
 
1654
1680
  ### 3.2 Extending Tables
1655
1681
 
@@ -1757,23 +1783,25 @@ if (!authorized) {
1757
1783
 
1758
1784
  ### 3.3 Programmatic Table Requests
1759
1785
 
1760
- Instructions for the agent to interact with Harper tables programmatically using the `tables` object, including querying, transactions, and module integration.
1786
+ Instructions for the agent to interact with Harper tables programmatically using the `tables` object, the Query API, and transactions.
1761
1787
 
1762
1788
  #### When to Use
1763
1789
 
1764
- Apply this rule when writing server-side Harper component code that reads from or writes to tables directly — bypassing REST endpoints — such as in request handlers, background jobs, timers, or SSR rendering. Use it whenever you need to construct queries with `conditions`, manage transactions explicitly, or perform CRDT-safe mutations.
1790
+ Apply this rule when writing server-side Harper component code that reads from or writes to tables directly — for example, in HTTP handlers, background jobs, timers, or SSR render functions. Use it whenever you need to construct queries with `conditions`, `sort`, `select`, `limit`, or `offset`, or when you need explicit transaction control via `transaction()`.
1765
1791
 
1766
1792
  #### How It Works
1767
1793
 
1768
- 1. **Import `tables` from `harper`**: Access all tables in the default `data` database via the `tables` object. Each table defined with `@table` in `schema.graphql` is a property.
1794
+ 1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a named property of `tables`. Each property is the table class implementing the Resource API.
1769
1795
 
1770
1796
  ```javascript
1771
- import { tables } from 'harper';
1797
+ import { tables, transaction } from 'harper';
1772
1798
  const { Product } = tables;
1773
- // same as: databases.data.Product
1799
+ // equivalent to: databases.data.Product
1774
1800
  ```
1775
1801
 
1776
- 2. **Define your schema with `@table`**: Tables must be declared in `schema.graphql`. Use `@indexed` on attributes you intend to sort or filter efficiently.
1802
+ For standalone components, run `npm link harper` so imports resolve to the live runtime.
1803
+
1804
+ 2. **Define your schema with `@table`**: Tables must be declared in `schema.graphql`. Mark attributes you intend to sort or filter on with `@indexed`.
1777
1805
 
1778
1806
  ```graphql
1779
1807
  type Product @table {
@@ -1783,7 +1811,7 @@ Apply this rule when writing server-side Harper component code that reads from o
1783
1811
  }
1784
1812
  ```
1785
1813
 
1786
- 3. **Use `search(` to query records**: Pass a Query object to `search(`. Iterate results with `for await`.
1814
+ 3. **Query records with `search(`**: Pass a Query object to `search(`. Iterate results with `for await`.
1787
1815
 
1788
1816
  ```javascript
1789
1817
  const query = {
@@ -1794,7 +1822,7 @@ Apply this rule when writing server-side Harper component code that reads from o
1794
1822
  }
1795
1823
  ```
1796
1824
 
1797
- 4. **Build `conditions` arrays to filter**: Each condition object supports these properties:
1825
+ 4. **Build `conditions`**: Each condition object supports these properties:
1798
1826
 
1799
1827
  | Property | Description |
1800
1828
  | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -1804,87 +1832,79 @@ Apply this rule when writing server-side Harper component code that reads from o
1804
1832
  | `conditions` | Nested conditions array |
1805
1833
  | `operator` | `and` (default) or `or` for the nested `conditions` |
1806
1834
 
1807
- 5. **Apply `select` to shape results**: Return only the fields you need. Supports arrays, nested relationship selects, and special properties.
1835
+ 5. **Control result shape with `select`**: Pass an array of property names, a single string, or nested objects for relationships.
1808
1836
 
1809
1837
  ```javascript
1810
- // Array of fields
1838
+ // Scalar fields only
1811
1839
  Product.search({ select: ['name', 'price'] });
1812
1840
 
1813
- // Nested relationship select
1841
+ // Partial related record
1814
1842
  Book.get({ id: 42, select: ['id', 'title', { name: 'author', select: ['name'] }] });
1815
1843
  ```
1816
1844
 
1817
1845
  Special `select` values: `$id`, `$updatedtime`, `$distance`.
1818
1846
 
1819
- 6. **Apply `sort` with an `@indexed` attribute**: Harper uses an index to provide sort order. Sort by an `@indexed` attribute without requiring a condition, or provide at least one condition when sorting by a non-indexed attribute.
1847
+ 6. **Paginate with `limit` and `offset`**:
1820
1848
 
1821
1849
  ```javascript
1822
- // Sort by primary key with an open-ended condition to avoid scan error
1823
- Product.search({
1824
- conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
1825
- sort: { attribute: 'id' },
1826
- });
1850
+ Product.search({ conditions: [...], limit: 20, offset: 40 });
1827
1851
  ```
1828
1852
 
1829
- Sort object properties:
1853
+ 7. **Sort with `sort`**: The `sort` object accepts `attribute`, `descending` (default `false`), and `next` for tie-breaking. Harper uses an index for sort order — the sort `attribute` must be `@indexed`, **or** at least one entry in `conditions` must be present.
1830
1854
 
1831
- | Property | Description |
1832
- | ------------ | -------------------------------------------------------- |
1833
- | `attribute` | Property name or array for chained relationship property |
1834
- | `descending` | Sort descending if `true` (default: `false`) |
1835
- | `next` | Secondary sort to resolve ties (same structure) |
1855
+ Sorting by a non-indexed attribute with zero conditions raises:
1836
1856
 
1837
- 7. **Use `limit` and `offset` for pagination**:
1857
+ > `HdbError: <attribute> is not indexed and not combined with any other conditions`
1858
+
1859
+ Note: `@primaryKey` alone is treated as not indexed for sort purposes. To scan the whole table in primary-key order, add an open-ended condition:
1838
1860
 
1839
1861
  ```javascript
1840
- Product.search({ conditions: [...], limit: 20, offset: 40 });
1862
+ Product.search({
1863
+ conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
1864
+ sort: { attribute: 'id' },
1865
+ });
1841
1866
  ```
1842
1867
 
1843
- 8. **Use `explain` and `enforceExecutionOrder` for debugging**:
1868
+ Alternatively, pass `allowFullScan: true` to permit an unconditional ordered scan, or omit `sort` entirely to iterate without an index requirement.
1869
+
1870
+ 8. **Debug query planning with `explain` and `enforceExecutionOrder`**:
1844
1871
  - `explain: true` — returns conditions reordered as Harper will execute them.
1845
1872
  - `enforceExecutionOrder: true` — forces conditions to execute in the order supplied, disabling automatic re-ordering.
1846
1873
 
1847
1874
  9. **Use `addTo` for concurrent-safe numeric updates**: `addTo` uses CRDT incrementation, safe across threads and nodes.
1848
1875
 
1849
1876
  ```javascript
1850
- static async post(target, data) {
1851
- const record = await this.update(target.id);
1852
- record.addTo('quantity', -1); // decrement safely across nodes
1853
- }
1877
+ const record = await Product.update(32);
1878
+ record.addTo('quantity', -1);
1854
1879
  ```
1855
1880
 
1856
- 10. **Wrap background work in `transaction()`**: HTTP handlers get a transaction automatically. Use `transaction()` explicitly for timers, background jobs, or any code outside a natural transaction context.
1881
+ 10. **Wrap background work in `transaction()`**: HTTP handlers get a transaction automatically. Use `transaction()` explicitly for timers, background jobs, or any code outside a request context. Always `await` the call and `catch` errors.
1857
1882
 
1858
1883
  ```javascript
1859
- import { tables } from 'harper';
1860
- const { MyTable } = tables;
1861
-
1862
- if (isMainThread) {
1863
- setInterval(async () => {
1864
- let data = await (await fetch('https://example.com/data')).json();
1865
- transaction(async (txn) => {
1866
- for (let item of data) {
1867
- await MyTable.put(item, txn);
1868
- }
1869
- });
1870
- }, 3600000); // every hour
1871
- }
1884
+ await transaction(async (txn) => {
1885
+ for (let item of data) {
1886
+ await MyTable.put(item, txn);
1887
+ }
1888
+ });
1872
1889
  ```
1873
1890
 
1874
- The `txn` object members:
1891
+ The `txn` object exposes:
1875
1892
 
1876
- | Member | Type | Description |
1877
- | --------------------- | --------------- | ------------------------------------------------------ |
1878
- | `commit()` | `() => Promise` | Commits the current transaction |
1879
- | `abort()` | `() => void` | Aborts the transaction and resets it |
1880
- | `resetReadSnapshot()` | `() => void` | Resets the read snapshot to the latest committed state |
1881
- | `timestamp` | `number` | Timestamp associated with the current transaction |
1893
+ | Member | Description |
1894
+ | --------------------- | ------------------------------------------------------ |
1895
+ | `commit()` | Commits the current transaction |
1896
+ | `abort()` | Aborts and resets the transaction |
1897
+ | `resetReadSnapshot()` | Resets the read snapshot to the latest committed state |
1898
+ | `timestamp` | Timestamp associated with the current transaction |
1882
1899
 
1883
- 11. **Understand atomicity boundaries**: All tables within the same database share one transactional context — writes across multiple tables commit atomically. Tables in different databases each get their own transaction with no cross-database atomicity guarantee.
1900
+ **Atomicity**: all tables in the same database share one transactional context — writes across multiple tables commit atomically. Tables in different databases each get their own transaction with no cross-database atomicity guarantee.
1884
1901
 
1885
- 12. **Keep `harper` external in bundlers**: When using SSR bundlers, mark `harper` as external so it resolves to the live runtime. In `vite.config`:
1902
+ If `transaction()` is called with a context that already has an active transaction, it reuses that transaction — safe to call defensively.
1903
+
1904
+ 11. **Keep `harper` external when bundling for SSR**: In `vite.config`, mark `harper` as external so it resolves to the runtime rather than being bundled.
1886
1905
 
1887
1906
  ```javascript
1907
+ // vite.config
1888
1908
  ssr: {
1889
1909
  external: ['harper'];
1890
1910
  }
@@ -1892,7 +1912,31 @@ Apply this rule when writing server-side Harper component code that reads from o
1892
1912
 
1893
1913
  #### Examples
1894
1914
 
1895
- ##### Nested conditions query
1915
+ ##### Full CRUD sequence
1916
+
1917
+ ```javascript
1918
+ import { tables } from 'harper';
1919
+ const { Product } = tables;
1920
+
1921
+ // Create
1922
+ const created = await Product.create({ name: 'Shirt', price: 9.5 });
1923
+
1924
+ // Patch
1925
+ await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
1926
+
1927
+ // Retrieve by primary key
1928
+ const record = await Product.get(created.id);
1929
+
1930
+ // Query with conditions
1931
+ const query = {
1932
+ conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
1933
+ };
1934
+ for await (const record of Product.search(query)) {
1935
+ // process record
1936
+ }
1937
+ ```
1938
+
1939
+ ##### Nested conditions with `or`
1896
1940
 
1897
1941
  ```javascript
1898
1942
  Product.search({
@@ -1915,28 +1959,35 @@ Product.search({
1915
1959
  Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
1916
1960
  ```
1917
1961
 
1918
- ##### Full CRUD sequence
1962
+ ##### Background job with `transaction()`
1919
1963
 
1920
1964
  ```javascript
1921
- // Create a new record (id auto-generated)
1922
- const created = await Product.create({ name: 'Shirt', price: 9.5 });
1923
-
1924
- // Modify the record
1925
- await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
1926
-
1927
- // Retrieve by primary key
1928
- const record = await Product.get(created.id);
1929
-
1930
- // Query with conditions
1931
- const query = {
1932
- conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
1933
- };
1934
- for await (const record of Product.search(query)) {
1935
- // process record
1965
+ import { isMainThread } from 'node:worker_threads';
1966
+ import { tables, transaction } from 'harper';
1967
+ const { MyTable } = tables;
1968
+
1969
+ if (isMainThread) {
1970
+ let running = false;
1971
+ setInterval(async () => {
1972
+ if (running) return;
1973
+ running = true;
1974
+ try {
1975
+ let data = await (await fetch('https://example.com/data')).json();
1976
+ await transaction(async (txn) => {
1977
+ for (let item of data) {
1978
+ await MyTable.put(item, txn);
1979
+ }
1980
+ });
1981
+ } catch (error) {
1982
+ logger.error('hourly import failed', error);
1983
+ } finally {
1984
+ running = false;
1985
+ }
1986
+ }, 3600000);
1936
1987
  }
1937
1988
  ```
1938
1989
 
1939
- ##### SSR rendering with `tables`
1990
+ ##### SSR render with `tables`
1940
1991
 
1941
1992
  ```typescript
1942
1993
  import { tables } from 'harper';
@@ -1947,23 +1998,12 @@ export async function render(url: string): Promise<string> {
1947
1998
  }
1948
1999
  ```
1949
2000
 
1950
- ##### Mutable update with `addTo`
1951
-
1952
- ```javascript
1953
- const product = await Product.update(32);
1954
- product.status = 'active';
1955
- product.subtractFrom('quantity', 1);
1956
- product.save();
1957
- ```
1958
-
1959
2001
  #### Notes
1960
2002
 
1961
- - `tables` calls run in a trusted server-side context and do **not** automatically apply the target table's role permissions. Enforce authorization in your own application logic.
1962
- - Destructive operations (`update`, `patch`, `delete`) act on live data and are not easily reversible. Always scope with specific `conditions`, validate the affected set before writing, and gate behind authorization controls.
1963
- - Sorting by the bare `@primaryKey` alone with no conditions triggers `HdbError: <attribute> is not indexed and not combined with any other conditions`. Add an open-ended range condition or pass `allowFullScan: true` to permit an unconditional scan.
1964
- - Selecting a relationship field without filtering on it behaves as a **LEFT JOIN**; adding a condition on a related attribute behaves as an **INNER JOIN**.
1965
- - `transaction()` is safe to call defensively — if a transaction is already active on the context, it reuses it and executes the callback immediately.
1966
- - Link the `harper` package for correct typings in standalone component directories: `npm link harper`.
2003
+ - `tables` and `databases` do **not** automatically apply role permissions — calls run in a trusted server-side context. Apply your own authorization controls before exposing results.
2004
+ - Destructive operations (`update`, `patch`, `delete`) act on live data and are not easily reversible. Scope them with specific `conditions` and validate the affected set before writing.
2005
+ - `tables` is the same live, process-wide object whether accessed as a global or via `import { tables } from 'harper'`. A record written through one component is immediately visible to every other.
2006
+ - Run `npm link harper` for components in their own directory to ensure typings match the running installation.
1967
2007
 
1968
2008
  ### 3.4 TypeScript Type Stripping in Harper
1969
2009