@okrlinkhub/okrhub 0.1.0 → 0.1.2
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/README.md +81 -18
- package/dist/client/index.d.ts +346 -170
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +241 -173
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +490 -681
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/config.d.ts +37 -0
- package/dist/component/config.d.ts.map +1 -0
- package/dist/component/config.js +87 -0
- package/dist/component/config.js.map +1 -0
- package/dist/component/entities/batch.d.ts +24 -24
- package/dist/component/entities/index.d.ts +7 -8
- package/dist/component/entities/index.d.ts.map +1 -1
- package/dist/component/entities/index.js +7 -9
- package/dist/component/entities/index.js.map +1 -1
- package/dist/component/entities/indicatorForecasts.d.ts +16 -6
- package/dist/component/entities/indicatorForecasts.d.ts.map +1 -1
- package/dist/component/entities/indicatorForecasts.js +32 -2
- package/dist/component/entities/indicatorForecasts.js.map +1 -1
- package/dist/component/entities/indicatorValues.d.ts +16 -22
- package/dist/component/entities/indicatorValues.d.ts.map +1 -1
- package/dist/component/entities/indicatorValues.js +33 -41
- package/dist/component/entities/indicatorValues.js.map +1 -1
- package/dist/component/entities/indicators.d.ts +19 -27
- package/dist/component/entities/indicators.d.ts.map +1 -1
- package/dist/component/entities/indicators.js +24 -40
- package/dist/component/entities/indicators.js.map +1 -1
- package/dist/component/entities/initiatives.d.ts +135 -37
- package/dist/component/entities/initiatives.d.ts.map +1 -1
- package/dist/component/entities/initiatives.js +220 -45
- package/dist/component/entities/initiatives.js.map +1 -1
- package/dist/component/entities/keyResults.d.ts +46 -32
- package/dist/component/entities/keyResults.d.ts.map +1 -1
- package/dist/component/entities/keyResults.js +80 -42
- package/dist/component/entities/keyResults.js.map +1 -1
- package/dist/component/entities/milestones.d.ts +21 -31
- package/dist/component/entities/milestones.d.ts.map +1 -1
- package/dist/component/entities/milestones.js +33 -40
- package/dist/component/entities/milestones.js.map +1 -1
- package/dist/component/entities/objectives.d.ts +40 -24
- package/dist/component/entities/objectives.d.ts.map +1 -1
- package/dist/component/entities/objectives.js +52 -41
- package/dist/component/entities/objectives.js.map +1 -1
- package/dist/component/entities/risks.d.ts +92 -39
- package/dist/component/entities/risks.d.ts.map +1 -1
- package/dist/component/entities/risks.js +152 -46
- package/dist/component/entities/risks.js.map +1 -1
- package/dist/component/externalId.d.ts +1 -1
- package/dist/component/okrhub.d.ts +9 -9
- package/dist/component/okrhub.d.ts.map +1 -1
- package/dist/component/okrhub.js +12 -10
- package/dist/component/okrhub.js.map +1 -1
- package/dist/component/schema.d.ts +180 -153
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +18 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sync/http.d.ts +3 -3
- package/dist/component/sync/processor.d.ts +16 -5
- package/dist/component/sync/processor.d.ts.map +1 -1
- package/dist/component/sync/processor.js +44 -6
- package/dist/component/sync/processor.js.map +1 -1
- package/dist/component/sync/queue.d.ts +4 -4
- package/package.json +1 -1
- package/src/client/index.ts +283 -207
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +531 -825
- package/src/component/config.ts +93 -0
- package/src/component/entities/index.ts +0 -10
- package/src/component/entities/indicatorForecasts.ts +37 -2
- package/src/component/entities/indicatorValues.ts +38 -51
- package/src/component/entities/indicators.ts +24 -47
- package/src/component/entities/initiatives.ts +243 -62
- package/src/component/entities/keyResults.ts +93 -52
- package/src/component/entities/milestones.ts +37 -47
- package/src/component/entities/objectives.ts +57 -45
- package/src/component/entities/risks.ts +169 -57
- package/src/component/okrhub.ts +16 -11
- package/src/component/schema.ts +20 -0
- package/src/component/sync/processor.ts +51 -6
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ OKRHub is a Convex component that enables external applications to sync their OK
|
|
|
14
14
|
- **One-way sync**: Data flows from your app to LinkHub
|
|
15
15
|
- **Queue-based processing**: Async processing with retry logic
|
|
16
16
|
- **HMAC authentication**: Secure API communication with cryptographic signatures
|
|
17
|
-
- **
|
|
17
|
+
- **Deterministic external IDs**: Use your Convex `_id` as the externalId identifier for automatic idempotency
|
|
18
|
+
- **Idempotent create operations**: `create*` with an existing externalId returns the existing entity, no duplicates
|
|
18
19
|
- **Company isolation**: Each API key is scoped to a specific company
|
|
19
20
|
|
|
20
21
|
## Architecture
|
|
@@ -131,25 +132,26 @@ LINKHUB_SIGNING_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
131
132
|
### 4. Use in your app
|
|
132
133
|
|
|
133
134
|
```typescript
|
|
134
|
-
import { generateExternalId } from "@okrlinkhub/okrhub";
|
|
135
135
|
import { useMutation } from "convex/react";
|
|
136
136
|
import { api } from "../convex/_generated/api";
|
|
137
137
|
|
|
138
|
-
function CreateObjective() {
|
|
138
|
+
function CreateObjective({ teamId }: { teamId: string }) {
|
|
139
139
|
const insertObjective = useMutation(api.okrhub.insertObjective);
|
|
140
140
|
|
|
141
141
|
const handleCreate = async () => {
|
|
142
|
-
|
|
143
|
-
const teamExternalId =
|
|
142
|
+
// Deterministic externalId based on Convex IDs — idempotent
|
|
143
|
+
const teamExternalId = `my-app:team:${teamId}`;
|
|
144
|
+
const objectiveExternalId = `my-app:objective:${teamId}:revenue-growth`;
|
|
144
145
|
|
|
145
146
|
await insertObjective({
|
|
146
147
|
objective: {
|
|
147
|
-
externalId,
|
|
148
|
-
title: "Increase Revenue
|
|
148
|
+
externalId: objectiveExternalId,
|
|
149
|
+
title: "Increase Revenue",
|
|
149
150
|
description: "Focus on expanding sales channels",
|
|
150
151
|
teamExternalId,
|
|
151
152
|
},
|
|
152
153
|
});
|
|
154
|
+
// Calling again with the same externalId is safe (idempotent)
|
|
153
155
|
};
|
|
154
156
|
|
|
155
157
|
return <button onClick={handleCreate}>Create Objective</button>;
|
|
@@ -186,11 +188,55 @@ OKRHub uses HMAC-SHA256 authentication to secure communication with LinkHub.
|
|
|
186
188
|
- Rotate API keys periodically
|
|
187
189
|
- Use granular permissions when possible
|
|
188
190
|
|
|
189
|
-
## External ID Format
|
|
191
|
+
## External ID Format & Idempotency
|
|
190
192
|
|
|
191
|
-
All entities use external IDs in the format: `{sourceApp}:{entityType}:{
|
|
193
|
+
All entities use external IDs in the format: `{sourceApp}:{entityType}:{identifier}`
|
|
192
194
|
|
|
193
|
-
|
|
195
|
+
The `{identifier}` segment can be either:
|
|
196
|
+
|
|
197
|
+
- **Deterministic (recommended)**: use your Convex document `_id` or a semantic key, so the same input always produces the same externalId. This enables **idempotency** — calling `create*` with an existing `externalId` returns the existing entity instead of creating a duplicate.
|
|
198
|
+
- **Random UUID**: use `generateExternalId()` for one-off entities where deduplication is not needed.
|
|
199
|
+
|
|
200
|
+
### Deterministic ExternalIds (recommended)
|
|
201
|
+
|
|
202
|
+
For **Level 1** (users & teams) and **Level 2** (local tables mapped to OKR entities), use the Convex `_id` of your local record as the identifier:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Level 1 — users & teams: use the Convex _id directly
|
|
206
|
+
const userExternalId = `my-app:user:${userId}`; // e.g. "my-app:user:jd7abc..."
|
|
207
|
+
const teamExternalId = `my-app:team:${teamId}`; // e.g. "my-app:team:k4xdef..."
|
|
208
|
+
|
|
209
|
+
// Level 2 — local records mapped to OKR entities
|
|
210
|
+
const keyResultExternalId = `my-app:keyResult:${targetId}`;
|
|
211
|
+
|
|
212
|
+
// Level 3 — component-only entities with semantic keys
|
|
213
|
+
const indicatorExternalId = `my-app:indicator:revenue:${teamId}`;
|
|
214
|
+
const objectiveExternalId = `my-app:objective:${teamId}:revenue-growth`;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Because the `externalId` is derived from stable, existing values (Convex IDs, business keys), calling `create*` again with the same arguments is **idempotent** — the component detects the existing `externalId` and returns `{ existing: true, ... }` instead of creating a duplicate:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// First call: creates the entity
|
|
221
|
+
const result1 = await ctx.runMutation(
|
|
222
|
+
components.okrhub.okrhub.createObjective,
|
|
223
|
+
{ sourceApp: "my-app", externalId: `my-app:objective:${teamId}:revenue-growth`,
|
|
224
|
+
title: "Increase Revenue", description: "...", teamExternalId }
|
|
225
|
+
);
|
|
226
|
+
// result1 = { success: true, externalId: "...", existing: false }
|
|
227
|
+
|
|
228
|
+
// Second call with same externalId: returns existing entity (idempotent)
|
|
229
|
+
const result2 = await ctx.runMutation(
|
|
230
|
+
components.okrhub.okrhub.createObjective,
|
|
231
|
+
{ sourceApp: "my-app", externalId: `my-app:objective:${teamId}:revenue-growth`,
|
|
232
|
+
title: "Increase Revenue", description: "...", teamExternalId }
|
|
233
|
+
);
|
|
234
|
+
// result2 = { success: true, externalId: "...", existing: true }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Random ExternalIds (fallback)
|
|
238
|
+
|
|
239
|
+
For entities that don't have a natural key, use `generateExternalId()`:
|
|
194
240
|
|
|
195
241
|
```typescript
|
|
196
242
|
import {
|
|
@@ -199,7 +245,7 @@ import {
|
|
|
199
245
|
parseExternalId
|
|
200
246
|
} from "@okrlinkhub/okrhub";
|
|
201
247
|
|
|
202
|
-
// Generate a
|
|
248
|
+
// Generate a random external ID (UUID-based)
|
|
203
249
|
const id = generateExternalId("my-app", "objective");
|
|
204
250
|
// "my-app:objective:550e8400-e29b-41d4-a716-446655440000"
|
|
205
251
|
|
|
@@ -211,6 +257,8 @@ const parsed = parseExternalId(id);
|
|
|
211
257
|
// { sourceApp: "my-app", entityType: "objective", uuid: "..." }
|
|
212
258
|
```
|
|
213
259
|
|
|
260
|
+
> **Note:** Random UUIDs do not support idempotency — each call generates a different ID, so `create*` will always create a new entity. Prefer deterministic IDs whenever possible.
|
|
261
|
+
|
|
214
262
|
### Supported Entity Types
|
|
215
263
|
|
|
216
264
|
| Entity Type | Description |
|
|
@@ -362,11 +410,11 @@ Save the returned `apiKey`, `keyPrefix`, and `signingSecret`.
|
|
|
362
410
|
|
|
363
411
|
### 2. Create Reference Mappings
|
|
364
412
|
|
|
365
|
-
For each team/user/company referenced by your external IDs, create a mapping in LinkHub using `ingest:createMappingForSetup
|
|
413
|
+
For each team/user/company referenced by your external IDs, create a mapping in LinkHub using `ingest:createMappingForSetup`. Use the Convex `_id` of your local record as the identifier:
|
|
366
414
|
|
|
367
415
|
```json
|
|
368
416
|
{
|
|
369
|
-
"externalId": "my-app:team:
|
|
417
|
+
"externalId": "my-app:team:k4xdef123abc",
|
|
370
418
|
"entityType": "team",
|
|
371
419
|
"convexId": "existing_team_id_in_linkhub",
|
|
372
420
|
"tableName": "teams",
|
|
@@ -375,6 +423,8 @@ For each team/user/company referenced by your external IDs, create a mapping in
|
|
|
375
423
|
}
|
|
376
424
|
```
|
|
377
425
|
|
|
426
|
+
> **Tip:** Using the Convex `_id` (e.g. `k4xdef123abc`) instead of random UUIDs means the externalId is **deterministic** — you can always reconstruct it from the local record and re-sync safely.
|
|
427
|
+
|
|
378
428
|
### 3. Configure Environment
|
|
379
429
|
|
|
380
430
|
Add credentials to your `.env.local`:
|
|
@@ -387,22 +437,35 @@ LINKHUB_SIGNING_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
387
437
|
|
|
388
438
|
### 4. Test Sync
|
|
389
439
|
|
|
390
|
-
Insert an entity and process the queue:
|
|
440
|
+
Insert an entity and process the queue. Use a deterministic externalId for idempotency:
|
|
391
441
|
|
|
392
442
|
```typescript
|
|
393
|
-
//
|
|
443
|
+
const teamId = "k4xdef123abc"; // your local Convex team _id
|
|
444
|
+
|
|
445
|
+
// Insert — idempotent thanks to deterministic externalId
|
|
394
446
|
await insertObjective({
|
|
395
447
|
objective: {
|
|
396
|
-
externalId:
|
|
397
|
-
title: "
|
|
448
|
+
externalId: `my-app:objective:${teamId}:revenue-growth`,
|
|
449
|
+
title: "Increase Revenue",
|
|
398
450
|
description: "Testing the sync",
|
|
399
|
-
teamExternalId:
|
|
451
|
+
teamExternalId: `my-app:team:${teamId}`,
|
|
400
452
|
},
|
|
401
453
|
});
|
|
402
454
|
|
|
403
455
|
// Process
|
|
404
456
|
const result = await processSyncQueue({ batchSize: 10 });
|
|
405
457
|
// { processed: 1, succeeded: 1, failed: 0 }
|
|
458
|
+
|
|
459
|
+
// Insert again — same externalId, so no duplicate is created
|
|
460
|
+
await insertObjective({
|
|
461
|
+
objective: {
|
|
462
|
+
externalId: `my-app:objective:${teamId}:revenue-growth`,
|
|
463
|
+
title: "Increase Revenue",
|
|
464
|
+
description: "Testing the sync",
|
|
465
|
+
teamExternalId: `my-app:team:${teamId}`,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
// The component returns { existing: true } instead of creating a duplicate
|
|
406
469
|
```
|
|
407
470
|
|
|
408
471
|
## HTTP Routes (Optional)
|