@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.
- package/.agents/skills/harper-best-practices/AGENTS.md +362 -322
- package/.agents/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/.agents/skills/harper-best-practices/rules/checking-authentication.md +253 -209
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +103 -89
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +62 -3
- package/.agents/skills/harper-mcp/AGENTS.md +77 -31
- package/.agents/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/.agents/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/agent/skills/harper-best-practices/AGENTS.md +362 -322
- package/agent/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/agent/skills/harper-best-practices/rules/checking-authentication.md +253 -209
- package/agent/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +103 -89
- package/agent/skills/harper-best-practices/rules.manifest.yaml +62 -3
- package/agent/skills/harper-mcp/AGENTS.md +77 -31
- package/agent/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/agent/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/package.json +1 -1
- package/skills-lock.json +2 -2
|
@@ -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
|
|
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
|
|
24
|
-
- `POST /{TableName}/`: Creates a
|
|
25
|
-
- `PUT /{TableName}/{id}`:
|
|
26
|
-
- `PATCH /{TableName}/{id}`:
|
|
27
|
-
- `DELETE /{TableName}
|
|
28
|
-
- `DELETE /{TableName}
|
|
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,
|
|
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.
|
|
1248
|
+
##### 1. Reading the Current Authenticated User
|
|
1249
1249
|
|
|
1250
|
-
|
|
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
|
-
|
|
1252
|
+
**In a static verb**, read the user from the `context` argument instead — `context.user`:
|
|
1290
1253
|
|
|
1291
|
-
|
|
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
|
-
|
|
1294
|
-
{
|
|
1295
|
-
"operation": "create_authentication_tokens",
|
|
1296
|
-
"username": "username",
|
|
1297
|
-
"password": "password"
|
|
1298
|
-
}
|
|
1299
|
-
```
|
|
1262
|
+
##### 2. Reading the Full Context
|
|
1300
1263
|
|
|
1301
|
-
|
|
1264
|
+
**In an instance method**, call `getContext()` to retrieve the current context, which includes:
|
|
1302
1265
|
|
|
1303
|
-
|
|
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
|
-
|
|
1269
|
+
**In a static verb**, the context is the trailing argument:
|
|
1311
1270
|
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
1274
|
+
When triggered by HTTP, the context also exposes `url`, `method`, `headers`, `responseHeaders`, `pathname`, `host`, `ip`, `body`, `data`, `lastModified`, and `requestContext`.
|
|
1326
1275
|
|
|
1327
|
-
|
|
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
|
-
|
|
1278
|
+
Enable sessions in `harper-config.yaml`:
|
|
1337
1279
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1285
|
+
Use `context.login` in a static `post` verb to verify credentials and establish a session cookie:
|
|
1347
1286
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1318
|
+
```json
|
|
1319
|
+
{
|
|
1320
|
+
"operation": "create_authentication_tokens",
|
|
1321
|
+
"username": "username",
|
|
1322
|
+
"password": "password"
|
|
1323
|
+
}
|
|
1324
|
+
```
|
|
1423
1325
|
|
|
1424
|
-
|
|
1425
|
-
authentication:
|
|
1426
|
-
operationTokenTimeout: 1d # Default: 1 day
|
|
1427
|
-
refreshTokenTimeout: 30d # Default: 30 days
|
|
1428
|
-
```
|
|
1326
|
+
Response:
|
|
1429
1327
|
|
|
1430
|
-
|
|
1328
|
+
```json
|
|
1329
|
+
{
|
|
1330
|
+
"operation_token": "<jwt-operation-token>",
|
|
1331
|
+
"refresh_token": "<jwt-refresh-token>"
|
|
1332
|
+
}
|
|
1333
|
+
```
|
|
1431
1334
|
|
|
1432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1491
|
+
static async post(_target, data, context) {
|
|
1492
|
+
const { username, password } = (await data) ?? {};
|
|
1471
1493
|
try {
|
|
1472
|
-
await context.login(
|
|
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
|
-
|
|
1483
|
-
|
|
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,
|
|
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
|
-
- `
|
|
1496
|
-
- The `third argument
|
|
1497
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1601
|
+
5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
|
|
1559
1602
|
|
|
1560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1658
|
+
**Root-relative fixed route:**
|
|
1632
1659
|
|
|
1633
1660
|
```javascript
|
|
1634
|
-
|
|
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 {
|
|
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
|
-
-
|
|
1648
|
-
-
|
|
1649
|
-
-
|
|
1650
|
-
-
|
|
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,
|
|
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 —
|
|
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
|
|
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
|
-
//
|
|
1799
|
+
// equivalent to: databases.data.Product
|
|
1774
1800
|
```
|
|
1775
1801
|
|
|
1776
|
-
|
|
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. **
|
|
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
|
|
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. **
|
|
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
|
-
//
|
|
1838
|
+
// Scalar fields only
|
|
1811
1839
|
Product.search({ select: ['name', 'price'] });
|
|
1812
1840
|
|
|
1813
|
-
//
|
|
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. **
|
|
1847
|
+
6. **Paginate with `limit` and `offset`**:
|
|
1820
1848
|
|
|
1821
1849
|
```javascript
|
|
1822
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
1862
|
+
Product.search({
|
|
1863
|
+
conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
|
|
1864
|
+
sort: { attribute: 'id' },
|
|
1865
|
+
});
|
|
1841
1866
|
```
|
|
1842
1867
|
|
|
1843
|
-
|
|
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
|
-
|
|
1851
|
-
|
|
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
|
|
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
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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
|
|
1891
|
+
The `txn` object exposes:
|
|
1875
1892
|
|
|
1876
|
-
| Member |
|
|
1877
|
-
| --------------------- |
|
|
1878
|
-
| `commit()` |
|
|
1879
|
-
| `abort()` |
|
|
1880
|
-
| `resetReadSnapshot()` |
|
|
1881
|
-
| `timestamp` |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#####
|
|
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
|
-
#####
|
|
1962
|
+
##### Background job with `transaction()`
|
|
1919
1963
|
|
|
1920
1964
|
```javascript
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
|
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`
|
|
1962
|
-
- Destructive operations (`update`, `patch`, `delete`) act on live data and are not easily reversible.
|
|
1963
|
-
-
|
|
1964
|
-
-
|
|
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
|
|