@okrlinkhub/okrhub 0.1.0
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/LICENSE +201 -0
- package/README.md +579 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +593 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +704 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +72 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +1986 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/entities/batch.d.ts +121 -0
- package/dist/component/entities/batch.d.ts.map +1 -0
- package/dist/component/entities/batch.js +81 -0
- package/dist/component/entities/batch.js.map +1 -0
- package/dist/component/entities/index.d.ts +13 -0
- package/dist/component/entities/index.d.ts.map +1 -0
- package/dist/component/entities/index.js +22 -0
- package/dist/component/entities/index.js.map +1 -0
- package/dist/component/entities/indicatorForecasts.d.ts +61 -0
- package/dist/component/entities/indicatorForecasts.d.ts.map +1 -0
- package/dist/component/entities/indicatorForecasts.js +180 -0
- package/dist/component/entities/indicatorForecasts.js.map +1 -0
- package/dist/component/entities/indicatorValues.d.ts +77 -0
- package/dist/component/entities/indicatorValues.d.ts.map +1 -0
- package/dist/component/entities/indicatorValues.js +218 -0
- package/dist/component/entities/indicatorValues.js.map +1 -0
- package/dist/component/entities/indicators.d.ts +90 -0
- package/dist/component/entities/indicators.d.ts.map +1 -0
- package/dist/component/entities/indicators.js +239 -0
- package/dist/component/entities/indicators.js.map +1 -0
- package/dist/component/entities/initiatives.d.ts +103 -0
- package/dist/component/entities/initiatives.d.ts.map +1 -0
- package/dist/component/entities/initiatives.js +275 -0
- package/dist/component/entities/initiatives.js.map +1 -0
- package/dist/component/entities/keyResults.d.ts +111 -0
- package/dist/component/entities/keyResults.d.ts.map +1 -0
- package/dist/component/entities/keyResults.js +284 -0
- package/dist/component/entities/keyResults.js.map +1 -0
- package/dist/component/entities/milestones.d.ts +93 -0
- package/dist/component/entities/milestones.d.ts.map +1 -0
- package/dist/component/entities/milestones.js +249 -0
- package/dist/component/entities/milestones.js.map +1 -0
- package/dist/component/entities/objectives.d.ts +99 -0
- package/dist/component/entities/objectives.d.ts.map +1 -0
- package/dist/component/entities/objectives.js +261 -0
- package/dist/component/entities/objectives.js.map +1 -0
- package/dist/component/entities/risks.d.ts +126 -0
- package/dist/component/entities/risks.d.ts.map +1 -0
- package/dist/component/entities/risks.js +315 -0
- package/dist/component/entities/risks.js.map +1 -0
- package/dist/component/externalId.d.ts +79 -0
- package/dist/component/externalId.d.ts.map +1 -0
- package/dist/component/externalId.js +124 -0
- package/dist/component/externalId.js.map +1 -0
- package/dist/component/lib/hmac.d.ts +18 -0
- package/dist/component/lib/hmac.d.ts.map +1 -0
- package/dist/component/lib/hmac.js +35 -0
- package/dist/component/lib/hmac.js.map +1 -0
- package/dist/component/lib/index.d.ts +7 -0
- package/dist/component/lib/index.d.ts.map +1 -0
- package/dist/component/lib/index.js +6 -0
- package/dist/component/lib/index.js.map +1 -0
- package/dist/component/lib/types.d.ts +30 -0
- package/dist/component/lib/types.d.ts.map +1 -0
- package/dist/component/lib/types.js +7 -0
- package/dist/component/lib/types.js.map +1 -0
- package/dist/component/lib/validation.d.ts +15 -0
- package/dist/component/lib/validation.d.ts.map +1 -0
- package/dist/component/lib/validation.js +29 -0
- package/dist/component/lib/validation.js.map +1 -0
- package/dist/component/okrhub.d.ts +31 -0
- package/dist/component/okrhub.d.ts.map +1 -0
- package/dist/component/okrhub.js +45 -0
- package/dist/component/okrhub.js.map +1 -0
- package/dist/component/schema.d.ts +943 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +437 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/sync/http.d.ts +30 -0
- package/dist/component/sync/http.d.ts.map +1 -0
- package/dist/component/sync/http.js +114 -0
- package/dist/component/sync/http.js.map +1 -0
- package/dist/component/sync/index.d.ts +7 -0
- package/dist/component/sync/index.d.ts.map +1 -0
- package/dist/component/sync/index.js +7 -0
- package/dist/component/sync/index.js.map +1 -0
- package/dist/component/sync/processor.d.ts +20 -0
- package/dist/component/sync/processor.d.ts.map +1 -0
- package/dist/component/sync/processor.js +67 -0
- package/dist/component/sync/processor.js.map +1 -0
- package/dist/component/sync/queue.d.ts +40 -0
- package/dist/component/sync/queue.d.ts.map +1 -0
- package/dist/component/sync/queue.js +176 -0
- package/dist/component/sync/queue.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +117 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +1004 -0
- package/src/component/_generated/api.ts +88 -0
- package/src/component/_generated/component.ts +2685 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/entities/batch.ts +90 -0
- package/src/component/entities/index.ts +64 -0
- package/src/component/entities/indicatorForecasts.ts +205 -0
- package/src/component/entities/indicatorValues.ts +254 -0
- package/src/component/entities/indicators.ts +290 -0
- package/src/component/entities/initiatives.ts +342 -0
- package/src/component/entities/keyResults.ts +334 -0
- package/src/component/entities/milestones.ts +296 -0
- package/src/component/entities/objectives.ts +294 -0
- package/src/component/entities/risks.ts +383 -0
- package/src/component/externalId.ts +172 -0
- package/src/component/lib/hmac.ts +53 -0
- package/src/component/lib/index.ts +7 -0
- package/src/component/lib/types.ts +31 -0
- package/src/component/lib/validation.ts +41 -0
- package/src/component/okrhub.ts +110 -0
- package/src/component/schema.ts +574 -0
- package/src/component/sync/http.ts +138 -0
- package/src/component/sync/index.ts +11 -0
- package/src/component/sync/processor.ts +77 -0
- package/src/component/sync/queue.ts +201 -0
- package/src/react/index.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# @okrlinkhub/okrhub
|
|
2
|
+
|
|
3
|
+
> Convex component for syncing OKR data (Objectives, Key Results, Risks, Initiatives) to LinkHub via secure HMAC-authenticated API.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/@okrlinkhub%2Fokrhub)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://convex.dev/)
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
OKRHub is a Convex component that enables external applications to sync their OKR data to LinkHub. It provides:
|
|
13
|
+
|
|
14
|
+
- **One-way sync**: Data flows from your app to LinkHub
|
|
15
|
+
- **Queue-based processing**: Async processing with retry logic
|
|
16
|
+
- **HMAC authentication**: Secure API communication with cryptographic signatures
|
|
17
|
+
- **External ID mapping**: Use your own IDs, LinkHub handles the mapping
|
|
18
|
+
- **Company isolation**: Each API key is scoped to a specific company
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
24
|
+
│ Your App │ │ @okrlinkhub/okrhub │ │ LinkHub │
|
|
25
|
+
│ (Convex) │ │ (Component) │ │ (Server) │
|
|
26
|
+
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
27
|
+
│ │ │
|
|
28
|
+
│ insertObjective() │ │
|
|
29
|
+
│ ────────────────────────► │ │
|
|
30
|
+
│ │ Saves to syncQueue │
|
|
31
|
+
│ │ ─────────┐ │
|
|
32
|
+
│ │ │ │
|
|
33
|
+
│ │ ◄────────┘ │
|
|
34
|
+
│ │ │
|
|
35
|
+
│ processSyncQueue() │ │
|
|
36
|
+
│ ────────────────────────► │ │
|
|
37
|
+
│ │ POST /ingest/okr/v1/* │
|
|
38
|
+
│ │ Headers: │
|
|
39
|
+
│ │ X-OKRHub-Signature │
|
|
40
|
+
│ │ X-OKRHub-Version │
|
|
41
|
+
│ │ X-OKRHub-Key-Prefix │
|
|
42
|
+
│ │ ─────────────────────────►│
|
|
43
|
+
│ │ │ Verify HMAC
|
|
44
|
+
│ │ │ Create/Update entity
|
|
45
|
+
│ │ │ Create ID mapping
|
|
46
|
+
│ │ { success: true } │
|
|
47
|
+
│ │ ◄─────────────────────────│
|
|
48
|
+
│ │ │
|
|
49
|
+
│ Update syncQueue status │ │
|
|
50
|
+
│ ◄──────────────────────── │ │
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
- Node.js 18+
|
|
56
|
+
- Convex 1.31.6+
|
|
57
|
+
- React 18.3.1+ or 19.0.0+ (for React hooks)
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @okrlinkhub/okrhub convex
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or with yarn:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
yarn add @okrlinkhub/okrhub convex
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or with pnpm:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pnpm add @okrlinkhub/okrhub convex
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
### 1. Add the component to your Convex app
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// convex/convex.config.ts
|
|
83
|
+
import { defineApp } from "convex/server";
|
|
84
|
+
import okrhub from "@okrlinkhub/okrhub/convex.config";
|
|
85
|
+
|
|
86
|
+
const app = defineApp();
|
|
87
|
+
app.use(okrhub);
|
|
88
|
+
|
|
89
|
+
export default app;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. Expose the API in your app
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// convex/okrhub.ts
|
|
96
|
+
import { components } from "./_generated/api";
|
|
97
|
+
import { exposeApi } from "@okrlinkhub/okrhub";
|
|
98
|
+
|
|
99
|
+
export const {
|
|
100
|
+
insertObjective,
|
|
101
|
+
insertKeyResult,
|
|
102
|
+
insertRisk,
|
|
103
|
+
insertInitiative,
|
|
104
|
+
insertIndicator,
|
|
105
|
+
insertMilestone,
|
|
106
|
+
processSyncQueue,
|
|
107
|
+
getPendingSyncItems,
|
|
108
|
+
} = exposeApi(components.okrhub, {
|
|
109
|
+
auth: async (ctx, operation) => {
|
|
110
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
111
|
+
if (!identity) throw new Error("Unauthorized");
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3. Set environment variables
|
|
117
|
+
|
|
118
|
+
Create a `.env.local` file in your project root:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# LinkHub site endpoint URL
|
|
122
|
+
LINKHUB_API_URL=https://your-linkhub.convex.site
|
|
123
|
+
|
|
124
|
+
# API Key Prefix (first 12 characters of your API key)
|
|
125
|
+
LINKHUB_API_KEY_PREFIX=okr_xxxxxxxx
|
|
126
|
+
|
|
127
|
+
# Signing Secret for HMAC authentication
|
|
128
|
+
LINKHUB_SIGNING_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 4. Use in your app
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { generateExternalId } from "@okrlinkhub/okrhub";
|
|
135
|
+
import { useMutation } from "convex/react";
|
|
136
|
+
import { api } from "../convex/_generated/api";
|
|
137
|
+
|
|
138
|
+
function CreateObjective() {
|
|
139
|
+
const insertObjective = useMutation(api.okrhub.insertObjective);
|
|
140
|
+
|
|
141
|
+
const handleCreate = async () => {
|
|
142
|
+
const externalId = generateExternalId("my-app", "objective");
|
|
143
|
+
const teamExternalId = generateExternalId("my-app", "team");
|
|
144
|
+
|
|
145
|
+
await insertObjective({
|
|
146
|
+
objective: {
|
|
147
|
+
externalId,
|
|
148
|
+
title: "Increase Revenue Q1",
|
|
149
|
+
description: "Focus on expanding sales channels",
|
|
150
|
+
teamExternalId,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return <button onClick={handleCreate}>Create Objective</button>;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Authentication
|
|
160
|
+
|
|
161
|
+
OKRHub uses HMAC-SHA256 authentication to secure communication with LinkHub.
|
|
162
|
+
|
|
163
|
+
### How it works
|
|
164
|
+
|
|
165
|
+
1. **API Key Creation**: In LinkHub, create an API key which generates:
|
|
166
|
+
- `apiKey`: Full API key (e.g., `okr_9d78c3eb...`) - store securely
|
|
167
|
+
- `keyPrefix`: First 12 characters for identification
|
|
168
|
+
- `signingSecret`: HMAC signing secret (e.g., `whsec_...`) - used to sign requests
|
|
169
|
+
|
|
170
|
+
2. **Request Signing**: Every request is signed using the `signingSecret`:
|
|
171
|
+
```
|
|
172
|
+
signature = HMAC-SHA256(payload, signingSecret)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
3. **Headers Sent**:
|
|
176
|
+
- `X-OKRHub-Signature`: HMAC signature of the request body
|
|
177
|
+
- `X-OKRHub-Version`: Component version (for compatibility)
|
|
178
|
+
- `X-OKRHub-Key-Prefix`: API key prefix for identification
|
|
179
|
+
|
|
180
|
+
4. **Server Verification**: LinkHub verifies the signature using the stored `signingSecret`
|
|
181
|
+
|
|
182
|
+
### Security Best Practices
|
|
183
|
+
|
|
184
|
+
- Never expose the `signingSecret` in client-side code
|
|
185
|
+
- Use environment variables for all credentials
|
|
186
|
+
- Rotate API keys periodically
|
|
187
|
+
- Use granular permissions when possible
|
|
188
|
+
|
|
189
|
+
## External ID Format
|
|
190
|
+
|
|
191
|
+
All entities use external IDs in the format: `{sourceApp}:{entityType}:{uuid}`
|
|
192
|
+
|
|
193
|
+
Example: `my-app:objective:550e8400-e29b-41d4-a716-446655440000`
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import {
|
|
197
|
+
generateExternalId,
|
|
198
|
+
validateExternalId,
|
|
199
|
+
parseExternalId
|
|
200
|
+
} from "@okrlinkhub/okrhub";
|
|
201
|
+
|
|
202
|
+
// Generate a new external ID
|
|
203
|
+
const id = generateExternalId("my-app", "objective");
|
|
204
|
+
// "my-app:objective:550e8400-e29b-41d4-a716-446655440000"
|
|
205
|
+
|
|
206
|
+
// Validate format
|
|
207
|
+
const isValid = validateExternalId(id); // true
|
|
208
|
+
|
|
209
|
+
// Parse components
|
|
210
|
+
const parsed = parseExternalId(id);
|
|
211
|
+
// { sourceApp: "my-app", entityType: "objective", uuid: "..." }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Supported Entity Types
|
|
215
|
+
|
|
216
|
+
| Entity Type | Description |
|
|
217
|
+
|-------------|-------------|
|
|
218
|
+
| `objective` | Strategic objectives |
|
|
219
|
+
| `keyResult` | Key results linked to objectives |
|
|
220
|
+
| `risk` | Risks linked to key results |
|
|
221
|
+
| `initiative` | Initiatives to mitigate risks |
|
|
222
|
+
| `indicator` | Metrics and KPIs |
|
|
223
|
+
| `milestone` | Milestone targets for indicators |
|
|
224
|
+
| `team` | Teams (for reference mapping) |
|
|
225
|
+
| `user` | Users (for reference mapping) |
|
|
226
|
+
| `company` | Companies (for reference mapping) |
|
|
227
|
+
|
|
228
|
+
## Entity Payloads
|
|
229
|
+
|
|
230
|
+
### Objective
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
{
|
|
234
|
+
externalId: string; // Required: unique ID
|
|
235
|
+
title: string; // Required: objective title
|
|
236
|
+
description: string; // Required: objective description
|
|
237
|
+
teamExternalId: string; // Required: reference to team
|
|
238
|
+
createdAt?: number; // Optional: timestamp
|
|
239
|
+
updatedAt?: number; // Optional: timestamp
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Key Result
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
{
|
|
247
|
+
externalId: string; // Required
|
|
248
|
+
indicatorExternalId: string; // Required: linked indicator
|
|
249
|
+
teamExternalId: string; // Required: team reference
|
|
250
|
+
weight: number; // Required: weight (0-100)
|
|
251
|
+
objectiveExternalId?: string; // Optional: linked objective
|
|
252
|
+
impact?: number; // Optional
|
|
253
|
+
forecastValue?: number; // Optional
|
|
254
|
+
targetValue?: number; // Optional
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Risk
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
{
|
|
262
|
+
externalId: string; // Required
|
|
263
|
+
description: string; // Required
|
|
264
|
+
teamExternalId: string; // Required
|
|
265
|
+
priority: "lowest" | "low" | "medium" | "high" | "highest";
|
|
266
|
+
keyResultExternalId?: string; // Optional: linked key result
|
|
267
|
+
indicatorExternalId?: string; // Optional: linked indicator
|
|
268
|
+
triggerValue?: number; // Optional
|
|
269
|
+
isRed?: boolean; // Optional
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Initiative
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
{
|
|
277
|
+
externalId: string; // Required
|
|
278
|
+
description: string; // Required
|
|
279
|
+
teamExternalId: string; // Required
|
|
280
|
+
assigneeExternalId: string; // Required: user reference
|
|
281
|
+
createdByExternalId: string; // Required: user reference
|
|
282
|
+
priority: "lowest" | "low" | "medium" | "high" | "highest";
|
|
283
|
+
riskExternalId?: string; // Optional: linked risk
|
|
284
|
+
status?: "ON_TIME" | "OVERDUE" | "FINISHED";
|
|
285
|
+
externalUrl?: string; // Optional: external link
|
|
286
|
+
notes?: string; // Optional
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Indicator
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
{
|
|
294
|
+
externalId: string; // Required
|
|
295
|
+
companyExternalId: string; // Required: company reference
|
|
296
|
+
description: string; // Required
|
|
297
|
+
symbol: string; // Required: unit symbol
|
|
298
|
+
periodicity: "weekly" | "monthly" | "quarterly" | "semesterly" | "yearly";
|
|
299
|
+
assigneeExternalId?: string; // Optional: responsible user
|
|
300
|
+
isReverse?: boolean; // Optional: lower is better
|
|
301
|
+
type?: "OUTPUT" | "OUTCOME"; // Optional
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Processing the Sync Queue
|
|
306
|
+
|
|
307
|
+
Entities are first stored in a sync queue, then processed asynchronously. Set up a cron job for automatic processing:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// convex/crons.ts
|
|
311
|
+
import { cronJobs } from "convex/server";
|
|
312
|
+
import { api } from "./_generated/api";
|
|
313
|
+
|
|
314
|
+
const crons = cronJobs();
|
|
315
|
+
|
|
316
|
+
crons.interval(
|
|
317
|
+
"process okrhub sync queue",
|
|
318
|
+
{ minutes: 1 },
|
|
319
|
+
api.okrhub.processSyncQueue,
|
|
320
|
+
{ batchSize: 50 }
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
export default crons;
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Or process manually:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// From Dashboard or action
|
|
330
|
+
await ctx.runAction(api.okrhub.processSyncQueue, {
|
|
331
|
+
batchSize: 10,
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Queue States
|
|
336
|
+
|
|
337
|
+
| Status | Description |
|
|
338
|
+
|--------|-------------|
|
|
339
|
+
| `pending` | Waiting to be processed |
|
|
340
|
+
| `processing` | Currently being sent to LinkHub |
|
|
341
|
+
| `success` | Successfully synced |
|
|
342
|
+
| `failed` | Failed after max retries |
|
|
343
|
+
|
|
344
|
+
## Initial Setup
|
|
345
|
+
|
|
346
|
+
Before syncing entities, you need to set up reference mappings in LinkHub for entities that are referenced by external IDs (teams, users, companies).
|
|
347
|
+
|
|
348
|
+
### 1. Create API Key in LinkHub
|
|
349
|
+
|
|
350
|
+
In the LinkHub Convex Dashboard, call `apiKeys:createForSetup`:
|
|
351
|
+
|
|
352
|
+
```json
|
|
353
|
+
{
|
|
354
|
+
"name": "My App Integration",
|
|
355
|
+
"companyId": "your_company_id",
|
|
356
|
+
"createdByUserId": "your_user_id",
|
|
357
|
+
"permissions": ["ingest:all"]
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Save the returned `apiKey`, `keyPrefix`, and `signingSecret`.
|
|
362
|
+
|
|
363
|
+
### 2. Create Reference Mappings
|
|
364
|
+
|
|
365
|
+
For each team/user/company referenced by your external IDs, create a mapping in LinkHub using `ingest:createMappingForSetup`:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"externalId": "my-app:team:00000000-0000-0000-0000-000000000001",
|
|
370
|
+
"entityType": "team",
|
|
371
|
+
"convexId": "existing_team_id_in_linkhub",
|
|
372
|
+
"tableName": "teams",
|
|
373
|
+
"sourceApp": "my-app",
|
|
374
|
+
"companyId": "your_company_id"
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### 3. Configure Environment
|
|
379
|
+
|
|
380
|
+
Add credentials to your `.env.local`:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
LINKHUB_API_URL=https://your-linkhub.convex.site
|
|
384
|
+
LINKHUB_API_KEY_PREFIX=okr_xxxxxxxx
|
|
385
|
+
LINKHUB_SIGNING_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 4. Test Sync
|
|
389
|
+
|
|
390
|
+
Insert an entity and process the queue:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Insert
|
|
394
|
+
await insertObjective({
|
|
395
|
+
objective: {
|
|
396
|
+
externalId: generateExternalId("my-app", "objective"),
|
|
397
|
+
title: "Test Objective",
|
|
398
|
+
description: "Testing the sync",
|
|
399
|
+
teamExternalId: "my-app:team:00000000-0000-0000-0000-000000000001",
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Process
|
|
404
|
+
const result = await processSyncQueue({ batchSize: 10 });
|
|
405
|
+
// { processed: 1, succeeded: 1, failed: 0 }
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## HTTP Routes (Optional)
|
|
409
|
+
|
|
410
|
+
Register HTTP routes for REST API access:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// convex/http.ts
|
|
414
|
+
import { httpRouter } from "convex/server";
|
|
415
|
+
import { components } from "./_generated/api";
|
|
416
|
+
import { registerRoutes } from "@okrlinkhub/okrhub";
|
|
417
|
+
|
|
418
|
+
const http = httpRouter();
|
|
419
|
+
registerRoutes(http, components.okrhub, { pathPrefix: "/api/okrhub" });
|
|
420
|
+
|
|
421
|
+
export default http;
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Payload Validators
|
|
425
|
+
|
|
426
|
+
All payload types are validated using Convex validators:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import {
|
|
430
|
+
objectivePayloadValidator,
|
|
431
|
+
keyResultPayloadValidator,
|
|
432
|
+
riskPayloadValidator,
|
|
433
|
+
initiativePayloadValidator,
|
|
434
|
+
indicatorPayloadValidator,
|
|
435
|
+
milestonePayloadValidator,
|
|
436
|
+
} from "@okrlinkhub/okrhub/schema";
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Development
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
# Install dependencies
|
|
443
|
+
npm install
|
|
444
|
+
|
|
445
|
+
# Run dev server (backend + frontend + build watcher)
|
|
446
|
+
npm run dev
|
|
447
|
+
|
|
448
|
+
# Build
|
|
449
|
+
npm run build
|
|
450
|
+
|
|
451
|
+
# Run tests
|
|
452
|
+
npm test
|
|
453
|
+
|
|
454
|
+
# Type check
|
|
455
|
+
npm run typecheck
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Example App
|
|
459
|
+
|
|
460
|
+
The `example/` directory contains a working example app demonstrating the component usage.
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
cd example
|
|
464
|
+
npm install
|
|
465
|
+
npm run dev
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## API Reference
|
|
469
|
+
|
|
470
|
+
### Core Functions
|
|
471
|
+
|
|
472
|
+
#### `exposeApi(component, options)`
|
|
473
|
+
|
|
474
|
+
Exposes the OKRHub component API with authentication.
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import { exposeApi } from "@okrlinkhub/okrhub";
|
|
478
|
+
import { components } from "./_generated/api";
|
|
479
|
+
|
|
480
|
+
export const {
|
|
481
|
+
insertObjective,
|
|
482
|
+
insertKeyResult,
|
|
483
|
+
insertRisk,
|
|
484
|
+
insertInitiative,
|
|
485
|
+
insertIndicator,
|
|
486
|
+
insertMilestone,
|
|
487
|
+
insertIndicatorValue,
|
|
488
|
+
insertIndicatorForecast,
|
|
489
|
+
processSyncQueue,
|
|
490
|
+
getPendingSyncItems,
|
|
491
|
+
} = exposeApi(components.okrhub, {
|
|
492
|
+
auth: async (ctx, operation) => {
|
|
493
|
+
// Your authentication logic
|
|
494
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
495
|
+
if (!identity) throw new Error("Unauthorized");
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
#### External ID Utilities
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import {
|
|
504
|
+
generateExternalId,
|
|
505
|
+
validateExternalId,
|
|
506
|
+
parseExternalId,
|
|
507
|
+
extractSourceApp,
|
|
508
|
+
extractEntityType,
|
|
509
|
+
sameSourceApp,
|
|
510
|
+
OKRHUB_VERSION,
|
|
511
|
+
ENTITY_TYPES,
|
|
512
|
+
} from "@okrlinkhub/okrhub";
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
#### HTTP Routes Registration
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { registerRoutes } from "@okrlinkhub/okrhub";
|
|
519
|
+
|
|
520
|
+
registerRoutes(httpRouter, components.okrhub, {
|
|
521
|
+
pathPrefix: "/api/okrhub",
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### React Hooks
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
import { useOKRHub } from "@okrlinkhub/okrhub/react";
|
|
529
|
+
import { useQuery } from "convex/react";
|
|
530
|
+
|
|
531
|
+
function MyComponent() {
|
|
532
|
+
const { getPendingSyncItems } = useOKRHub();
|
|
533
|
+
const pendingItems = useQuery(getPendingSyncItems);
|
|
534
|
+
|
|
535
|
+
// Use pendingItems...
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Related Packages
|
|
540
|
+
|
|
541
|
+
- [@okrlinkhub/ui-kit](https://github.com/okrlinkhub/linkhub-ui-kit) - React components for displaying OKR data
|
|
542
|
+
|
|
543
|
+
## Troubleshooting
|
|
544
|
+
|
|
545
|
+
### "Team not found for externalId"
|
|
546
|
+
|
|
547
|
+
You need to create a mapping for the team before syncing entities that reference it. See [Initial Setup](#initial-setup).
|
|
548
|
+
|
|
549
|
+
### "Invalid signature"
|
|
550
|
+
|
|
551
|
+
Check that:
|
|
552
|
+
1. `LINKHUB_SIGNING_SECRET` matches the `signingSecret` from API key creation
|
|
553
|
+
2. `LINKHUB_API_KEY_PREFIX` matches the `keyPrefix` from API key creation
|
|
554
|
+
3. The API key is active and not expired
|
|
555
|
+
|
|
556
|
+
### "Client version too old"
|
|
557
|
+
|
|
558
|
+
Update the component: `npm update @okrlinkhub/okrhub`
|
|
559
|
+
|
|
560
|
+
### Queue items stuck in "processing" state
|
|
561
|
+
|
|
562
|
+
This can happen if the processing action crashes. Reset them manually:
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
// In Convex Dashboard
|
|
566
|
+
await ctx.runMutation(internal.okrhub.sync.resetStuckItems, {});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## Contributing
|
|
570
|
+
|
|
571
|
+
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
|
572
|
+
|
|
573
|
+
## Versioning
|
|
574
|
+
|
|
575
|
+
This project uses [Semantic Versioning](https://semver.org/). For the versions available, see the [CHANGELOG.md](CHANGELOG.md) file.
|
|
576
|
+
|
|
577
|
+
## License
|
|
578
|
+
|
|
579
|
+
This project is licensed under the Apache-2.0 License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=_ignore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_ignore.d.ts","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_ignore.js","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":";AAAA,kEAAkE"}
|