@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.
Files changed (83) hide show
  1. package/README.md +81 -18
  2. package/dist/client/index.d.ts +346 -170
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +241 -173
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -0
  7. package/dist/component/_generated/api.d.ts.map +1 -1
  8. package/dist/component/_generated/api.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +490 -681
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/config.d.ts +37 -0
  12. package/dist/component/config.d.ts.map +1 -0
  13. package/dist/component/config.js +87 -0
  14. package/dist/component/config.js.map +1 -0
  15. package/dist/component/entities/batch.d.ts +24 -24
  16. package/dist/component/entities/index.d.ts +7 -8
  17. package/dist/component/entities/index.d.ts.map +1 -1
  18. package/dist/component/entities/index.js +7 -9
  19. package/dist/component/entities/index.js.map +1 -1
  20. package/dist/component/entities/indicatorForecasts.d.ts +16 -6
  21. package/dist/component/entities/indicatorForecasts.d.ts.map +1 -1
  22. package/dist/component/entities/indicatorForecasts.js +32 -2
  23. package/dist/component/entities/indicatorForecasts.js.map +1 -1
  24. package/dist/component/entities/indicatorValues.d.ts +16 -22
  25. package/dist/component/entities/indicatorValues.d.ts.map +1 -1
  26. package/dist/component/entities/indicatorValues.js +33 -41
  27. package/dist/component/entities/indicatorValues.js.map +1 -1
  28. package/dist/component/entities/indicators.d.ts +19 -27
  29. package/dist/component/entities/indicators.d.ts.map +1 -1
  30. package/dist/component/entities/indicators.js +24 -40
  31. package/dist/component/entities/indicators.js.map +1 -1
  32. package/dist/component/entities/initiatives.d.ts +135 -37
  33. package/dist/component/entities/initiatives.d.ts.map +1 -1
  34. package/dist/component/entities/initiatives.js +220 -45
  35. package/dist/component/entities/initiatives.js.map +1 -1
  36. package/dist/component/entities/keyResults.d.ts +46 -32
  37. package/dist/component/entities/keyResults.d.ts.map +1 -1
  38. package/dist/component/entities/keyResults.js +80 -42
  39. package/dist/component/entities/keyResults.js.map +1 -1
  40. package/dist/component/entities/milestones.d.ts +21 -31
  41. package/dist/component/entities/milestones.d.ts.map +1 -1
  42. package/dist/component/entities/milestones.js +33 -40
  43. package/dist/component/entities/milestones.js.map +1 -1
  44. package/dist/component/entities/objectives.d.ts +40 -24
  45. package/dist/component/entities/objectives.d.ts.map +1 -1
  46. package/dist/component/entities/objectives.js +52 -41
  47. package/dist/component/entities/objectives.js.map +1 -1
  48. package/dist/component/entities/risks.d.ts +92 -39
  49. package/dist/component/entities/risks.d.ts.map +1 -1
  50. package/dist/component/entities/risks.js +152 -46
  51. package/dist/component/entities/risks.js.map +1 -1
  52. package/dist/component/externalId.d.ts +1 -1
  53. package/dist/component/okrhub.d.ts +9 -9
  54. package/dist/component/okrhub.d.ts.map +1 -1
  55. package/dist/component/okrhub.js +12 -10
  56. package/dist/component/okrhub.js.map +1 -1
  57. package/dist/component/schema.d.ts +180 -153
  58. package/dist/component/schema.d.ts.map +1 -1
  59. package/dist/component/schema.js +18 -0
  60. package/dist/component/schema.js.map +1 -1
  61. package/dist/component/sync/http.d.ts +3 -3
  62. package/dist/component/sync/processor.d.ts +16 -5
  63. package/dist/component/sync/processor.d.ts.map +1 -1
  64. package/dist/component/sync/processor.js +44 -6
  65. package/dist/component/sync/processor.js.map +1 -1
  66. package/dist/component/sync/queue.d.ts +4 -4
  67. package/package.json +1 -1
  68. package/src/client/index.ts +283 -207
  69. package/src/component/_generated/api.ts +2 -0
  70. package/src/component/_generated/component.ts +531 -825
  71. package/src/component/config.ts +93 -0
  72. package/src/component/entities/index.ts +0 -10
  73. package/src/component/entities/indicatorForecasts.ts +37 -2
  74. package/src/component/entities/indicatorValues.ts +38 -51
  75. package/src/component/entities/indicators.ts +24 -47
  76. package/src/component/entities/initiatives.ts +243 -62
  77. package/src/component/entities/keyResults.ts +93 -52
  78. package/src/component/entities/milestones.ts +37 -47
  79. package/src/component/entities/objectives.ts +57 -45
  80. package/src/component/entities/risks.ts +169 -57
  81. package/src/component/okrhub.ts +16 -11
  82. package/src/component/schema.ts +20 -0
  83. 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
- - **External ID mapping**: Use your own IDs, LinkHub handles the mapping
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
- const externalId = generateExternalId("my-app", "objective");
143
- const teamExternalId = generateExternalId("my-app", "team");
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 Q1",
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}:{uuid}`
193
+ All entities use external IDs in the format: `{sourceApp}:{entityType}:{identifier}`
192
194
 
193
- Example: `my-app:objective:550e8400-e29b-41d4-a716-446655440000`
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 new external ID
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:00000000-0000-0000-0000-000000000001",
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
- // Insert
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: generateExternalId("my-app", "objective"),
397
- title: "Test Objective",
448
+ externalId: `my-app:objective:${teamId}:revenue-growth`,
449
+ title: "Increase Revenue",
398
450
  description: "Testing the sync",
399
- teamExternalId: "my-app:team:00000000-0000-0000-0000-000000000001",
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)