@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
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risks Entity for OKRHub Component
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations and queries for Risks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { v } from "convex/values";
|
|
8
|
+
import { mutation, query } from "../_generated/server.js";
|
|
9
|
+
import type { Id } from "../_generated/dataModel.js";
|
|
10
|
+
import { generateExternalId } from "../externalId.js";
|
|
11
|
+
import { assertValidExternalId, generateSlug } from "../lib/validation.js";
|
|
12
|
+
import {
|
|
13
|
+
riskPayloadValidator,
|
|
14
|
+
PrioritySchema,
|
|
15
|
+
SyncStatusSchema,
|
|
16
|
+
} from "../schema.js";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// LOCAL CRUD MUTATIONS
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a risk locally and queues for sync
|
|
24
|
+
*/
|
|
25
|
+
export const createRisk = mutation({
|
|
26
|
+
args: {
|
|
27
|
+
sourceApp: v.string(),
|
|
28
|
+
sourceUrl: v.optional(v.string()),
|
|
29
|
+
description: v.string(),
|
|
30
|
+
teamExternalId: v.string(),
|
|
31
|
+
keyResultExternalId: v.string(), // Required: Reference to key result
|
|
32
|
+
priority: PrioritySchema,
|
|
33
|
+
indicatorExternalId: v.optional(v.string()),
|
|
34
|
+
triggerValue: v.optional(v.number()),
|
|
35
|
+
triggeredIfLower: v.optional(v.boolean()),
|
|
36
|
+
useForecastAsTrigger: v.optional(v.boolean()),
|
|
37
|
+
isRed: v.optional(v.boolean()),
|
|
38
|
+
},
|
|
39
|
+
returns: v.object({
|
|
40
|
+
success: v.boolean(),
|
|
41
|
+
externalId: v.string(),
|
|
42
|
+
localId: v.id("risks"),
|
|
43
|
+
queueId: v.optional(v.id("syncQueue")),
|
|
44
|
+
error: v.optional(v.string()),
|
|
45
|
+
}),
|
|
46
|
+
handler: async (ctx, args) => {
|
|
47
|
+
const {
|
|
48
|
+
sourceApp,
|
|
49
|
+
sourceUrl,
|
|
50
|
+
description,
|
|
51
|
+
teamExternalId,
|
|
52
|
+
keyResultExternalId,
|
|
53
|
+
priority,
|
|
54
|
+
indicatorExternalId,
|
|
55
|
+
triggerValue,
|
|
56
|
+
triggeredIfLower,
|
|
57
|
+
useForecastAsTrigger,
|
|
58
|
+
isRed,
|
|
59
|
+
} = args;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
assertValidExternalId(teamExternalId, "teamExternalId");
|
|
63
|
+
assertValidExternalId(keyResultExternalId, "keyResultExternalId");
|
|
64
|
+
if (indicatorExternalId) {
|
|
65
|
+
assertValidExternalId(indicatorExternalId, "indicatorExternalId");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const externalId = generateExternalId(sourceApp, "risk");
|
|
69
|
+
const slug = generateSlug(sourceApp, description.substring(0, 30));
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
|
|
72
|
+
const localId = await ctx.db.insert("risks", {
|
|
73
|
+
externalId,
|
|
74
|
+
description,
|
|
75
|
+
teamExternalId,
|
|
76
|
+
keyResultExternalId,
|
|
77
|
+
priority,
|
|
78
|
+
indicatorExternalId,
|
|
79
|
+
triggerValue,
|
|
80
|
+
triggeredIfLower,
|
|
81
|
+
useForecastAsTrigger,
|
|
82
|
+
isRed,
|
|
83
|
+
slug,
|
|
84
|
+
syncStatus: "pending",
|
|
85
|
+
createdAt: now,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const payload = JSON.stringify({
|
|
89
|
+
externalId,
|
|
90
|
+
description,
|
|
91
|
+
teamExternalId,
|
|
92
|
+
keyResultExternalId,
|
|
93
|
+
priority,
|
|
94
|
+
indicatorExternalId,
|
|
95
|
+
triggerValue,
|
|
96
|
+
triggeredIfLower,
|
|
97
|
+
useForecastAsTrigger,
|
|
98
|
+
isRed,
|
|
99
|
+
sourceUrl,
|
|
100
|
+
createdAt: now,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const queueId = await ctx.db.insert("syncQueue", {
|
|
104
|
+
entityType: "risk",
|
|
105
|
+
externalId,
|
|
106
|
+
payload,
|
|
107
|
+
status: "pending",
|
|
108
|
+
attempts: 0,
|
|
109
|
+
createdAt: now,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
externalId,
|
|
115
|
+
localId,
|
|
116
|
+
queueId,
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const errorMessage =
|
|
120
|
+
error && typeof error === "object" && "message" in error
|
|
121
|
+
? (error.message as string)
|
|
122
|
+
: "Unknown error";
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
externalId: "",
|
|
127
|
+
localId: "" as Id<"risks">,
|
|
128
|
+
error: errorMessage,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// LOCAL QUERY FUNCTIONS
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Gets all local risks for a key result
|
|
140
|
+
*/
|
|
141
|
+
export const getRisksByKeyResult = query({
|
|
142
|
+
args: {
|
|
143
|
+
keyResultExternalId: v.string(),
|
|
144
|
+
},
|
|
145
|
+
returns: v.array(
|
|
146
|
+
v.object({
|
|
147
|
+
_id: v.id("risks"),
|
|
148
|
+
_creationTime: v.number(),
|
|
149
|
+
externalId: v.string(),
|
|
150
|
+
description: v.string(),
|
|
151
|
+
teamExternalId: v.string(),
|
|
152
|
+
keyResultExternalId: v.string(), // Required
|
|
153
|
+
priority: PrioritySchema,
|
|
154
|
+
indicatorExternalId: v.optional(v.string()),
|
|
155
|
+
triggerValue: v.optional(v.number()),
|
|
156
|
+
triggeredIfLower: v.optional(v.boolean()),
|
|
157
|
+
useForecastAsTrigger: v.optional(v.boolean()),
|
|
158
|
+
isRed: v.optional(v.boolean()),
|
|
159
|
+
slug: v.string(),
|
|
160
|
+
syncStatus: SyncStatusSchema,
|
|
161
|
+
createdAt: v.number(),
|
|
162
|
+
deletedAt: v.optional(v.number()),
|
|
163
|
+
})
|
|
164
|
+
),
|
|
165
|
+
handler: async (ctx, args) => {
|
|
166
|
+
return await ctx.db
|
|
167
|
+
.query("risks")
|
|
168
|
+
.withIndex("by_key_result", (q) =>
|
|
169
|
+
q.eq("keyResultExternalId", args.keyResultExternalId)
|
|
170
|
+
)
|
|
171
|
+
.filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
172
|
+
.collect();
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Gets all local risks
|
|
178
|
+
*/
|
|
179
|
+
export const getAllRisks = query({
|
|
180
|
+
args: {},
|
|
181
|
+
returns: v.array(
|
|
182
|
+
v.object({
|
|
183
|
+
_id: v.id("risks"),
|
|
184
|
+
_creationTime: v.number(),
|
|
185
|
+
externalId: v.string(),
|
|
186
|
+
description: v.string(),
|
|
187
|
+
teamExternalId: v.string(),
|
|
188
|
+
keyResultExternalId: v.string(), // Required
|
|
189
|
+
priority: PrioritySchema,
|
|
190
|
+
indicatorExternalId: v.optional(v.string()),
|
|
191
|
+
triggerValue: v.optional(v.number()),
|
|
192
|
+
triggeredIfLower: v.optional(v.boolean()),
|
|
193
|
+
useForecastAsTrigger: v.optional(v.boolean()),
|
|
194
|
+
isRed: v.optional(v.boolean()),
|
|
195
|
+
slug: v.string(),
|
|
196
|
+
syncStatus: SyncStatusSchema,
|
|
197
|
+
createdAt: v.number(),
|
|
198
|
+
deletedAt: v.optional(v.number()),
|
|
199
|
+
})
|
|
200
|
+
),
|
|
201
|
+
handler: async (ctx) => {
|
|
202
|
+
return await ctx.db
|
|
203
|
+
.query("risks")
|
|
204
|
+
.filter((q) => q.eq(q.field("deletedAt"), undefined))
|
|
205
|
+
.collect();
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// UPDATE MUTATIONS
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Updates a risk locally and queues for sync
|
|
215
|
+
* Resets syncStatus to "pending"
|
|
216
|
+
*/
|
|
217
|
+
export const updateRisk = mutation({
|
|
218
|
+
args: {
|
|
219
|
+
externalId: v.string(),
|
|
220
|
+
description: v.optional(v.string()),
|
|
221
|
+
priority: v.optional(PrioritySchema),
|
|
222
|
+
keyResultExternalId: v.optional(v.string()),
|
|
223
|
+
indicatorExternalId: v.optional(v.string()),
|
|
224
|
+
triggerValue: v.optional(v.number()),
|
|
225
|
+
triggeredIfLower: v.optional(v.boolean()),
|
|
226
|
+
useForecastAsTrigger: v.optional(v.boolean()),
|
|
227
|
+
isRed: v.optional(v.boolean()),
|
|
228
|
+
},
|
|
229
|
+
returns: v.object({
|
|
230
|
+
success: v.boolean(),
|
|
231
|
+
externalId: v.string(),
|
|
232
|
+
queueId: v.optional(v.id("syncQueue")),
|
|
233
|
+
error: v.optional(v.string()),
|
|
234
|
+
}),
|
|
235
|
+
handler: async (ctx, args) => {
|
|
236
|
+
const {
|
|
237
|
+
externalId,
|
|
238
|
+
description,
|
|
239
|
+
priority,
|
|
240
|
+
keyResultExternalId,
|
|
241
|
+
indicatorExternalId,
|
|
242
|
+
triggerValue,
|
|
243
|
+
triggeredIfLower,
|
|
244
|
+
useForecastAsTrigger,
|
|
245
|
+
isRed,
|
|
246
|
+
} = args;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Find the risk by externalId
|
|
250
|
+
const risk = await ctx.db
|
|
251
|
+
.query("risks")
|
|
252
|
+
.withIndex("by_external_id", (q) => q.eq("externalId", externalId))
|
|
253
|
+
.first();
|
|
254
|
+
|
|
255
|
+
if (!risk) {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
externalId,
|
|
259
|
+
error: `Risk not found: ${externalId}`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Validate external IDs if provided
|
|
264
|
+
if (keyResultExternalId) {
|
|
265
|
+
assertValidExternalId(keyResultExternalId, "keyResultExternalId");
|
|
266
|
+
}
|
|
267
|
+
if (indicatorExternalId) {
|
|
268
|
+
assertValidExternalId(indicatorExternalId, "indicatorExternalId");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
|
|
273
|
+
// Update the risk
|
|
274
|
+
await ctx.db.patch(risk._id, {
|
|
275
|
+
...(description !== undefined && { description }),
|
|
276
|
+
...(priority !== undefined && { priority }),
|
|
277
|
+
...(keyResultExternalId !== undefined && { keyResultExternalId }),
|
|
278
|
+
...(indicatorExternalId !== undefined && { indicatorExternalId }),
|
|
279
|
+
...(triggerValue !== undefined && { triggerValue }),
|
|
280
|
+
...(triggeredIfLower !== undefined && { triggeredIfLower }),
|
|
281
|
+
...(useForecastAsTrigger !== undefined && { useForecastAsTrigger }),
|
|
282
|
+
...(isRed !== undefined && { isRed }),
|
|
283
|
+
syncStatus: "pending",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Create payload for sync with updated values
|
|
287
|
+
const updatedRisk = {
|
|
288
|
+
externalId,
|
|
289
|
+
description: description ?? risk.description,
|
|
290
|
+
teamExternalId: risk.teamExternalId,
|
|
291
|
+
priority: priority ?? risk.priority,
|
|
292
|
+
keyResultExternalId: keyResultExternalId ?? risk.keyResultExternalId,
|
|
293
|
+
indicatorExternalId: indicatorExternalId ?? risk.indicatorExternalId,
|
|
294
|
+
triggerValue: triggerValue ?? risk.triggerValue,
|
|
295
|
+
triggeredIfLower: triggeredIfLower ?? risk.triggeredIfLower,
|
|
296
|
+
useForecastAsTrigger: useForecastAsTrigger ?? risk.useForecastAsTrigger,
|
|
297
|
+
isRed: isRed ?? risk.isRed,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const payload = JSON.stringify(updatedRisk);
|
|
301
|
+
|
|
302
|
+
// Add to sync queue
|
|
303
|
+
const queueId = await ctx.db.insert("syncQueue", {
|
|
304
|
+
entityType: "risk",
|
|
305
|
+
externalId,
|
|
306
|
+
payload,
|
|
307
|
+
status: "pending",
|
|
308
|
+
attempts: 0,
|
|
309
|
+
createdAt: now,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
externalId,
|
|
315
|
+
queueId,
|
|
316
|
+
};
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const errorMessage =
|
|
319
|
+
error && typeof error === "object" && "message" in error
|
|
320
|
+
? (error.message as string)
|
|
321
|
+
: "Unknown error";
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
externalId,
|
|
326
|
+
error: errorMessage,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// PUBLIC MUTATIONS - Entry points for consumers
|
|
334
|
+
// ============================================================================
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Insert a risk into LinkHub
|
|
338
|
+
*/
|
|
339
|
+
export const insertRisk = mutation({
|
|
340
|
+
args: {
|
|
341
|
+
risk: riskPayloadValidator,
|
|
342
|
+
},
|
|
343
|
+
returns: v.object({
|
|
344
|
+
success: v.boolean(),
|
|
345
|
+
externalId: v.string(),
|
|
346
|
+
queueId: v.optional(v.id("syncQueue")),
|
|
347
|
+
error: v.optional(v.string()),
|
|
348
|
+
}),
|
|
349
|
+
handler: async (ctx, args) => {
|
|
350
|
+
const { risk } = args;
|
|
351
|
+
|
|
352
|
+
// Validate external IDs
|
|
353
|
+
assertValidExternalId(risk.externalId, "risk.externalId");
|
|
354
|
+
assertValidExternalId(risk.teamExternalId, "risk.teamExternalId");
|
|
355
|
+
assertValidExternalId(
|
|
356
|
+
risk.keyResultExternalId,
|
|
357
|
+
"risk.keyResultExternalId"
|
|
358
|
+
);
|
|
359
|
+
if (risk.indicatorExternalId) {
|
|
360
|
+
assertValidExternalId(
|
|
361
|
+
risk.indicatorExternalId,
|
|
362
|
+
"risk.indicatorExternalId"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Add to sync queue
|
|
367
|
+
const payload = JSON.stringify(risk);
|
|
368
|
+
const queueId = await ctx.db.insert("syncQueue", {
|
|
369
|
+
entityType: "risk",
|
|
370
|
+
externalId: risk.externalId,
|
|
371
|
+
payload,
|
|
372
|
+
status: "pending",
|
|
373
|
+
attempts: 0,
|
|
374
|
+
createdAt: Date.now(),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
success: true,
|
|
379
|
+
externalId: risk.externalId,
|
|
380
|
+
queueId,
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External ID helper for OKRHub component
|
|
3
|
+
*
|
|
4
|
+
* External IDs are used to map entities between the consumer app and LinkHub.
|
|
5
|
+
* Format: {sourceApp}:{entityType}:{uuid}
|
|
6
|
+
*
|
|
7
|
+
* Example: "mycrm:objective:550e8400-e29b-41d4-a716-446655440000"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { v } from "convex/values";
|
|
11
|
+
|
|
12
|
+
// Entity types supported by OKRHub
|
|
13
|
+
export const ENTITY_TYPES = [
|
|
14
|
+
"objective",
|
|
15
|
+
"keyResult",
|
|
16
|
+
"risk",
|
|
17
|
+
"initiative",
|
|
18
|
+
"indicator",
|
|
19
|
+
"indicatorValue",
|
|
20
|
+
"indicatorForecast",
|
|
21
|
+
"milestone",
|
|
22
|
+
"team",
|
|
23
|
+
"company",
|
|
24
|
+
"user",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type EntityType = (typeof ENTITY_TYPES)[number];
|
|
28
|
+
|
|
29
|
+
// Convex validator for entity type
|
|
30
|
+
export const entityTypeValidator = v.union(
|
|
31
|
+
v.literal("objective"),
|
|
32
|
+
v.literal("keyResult"),
|
|
33
|
+
v.literal("risk"),
|
|
34
|
+
v.literal("initiative"),
|
|
35
|
+
v.literal("indicator"),
|
|
36
|
+
v.literal("indicatorValue"),
|
|
37
|
+
v.literal("indicatorForecast"),
|
|
38
|
+
v.literal("milestone"),
|
|
39
|
+
v.literal("team"),
|
|
40
|
+
v.literal("company"),
|
|
41
|
+
v.literal("user")
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Regex pattern for external ID validation
|
|
45
|
+
// Format: {sourceApp}:{entityType}:{uuid}
|
|
46
|
+
// - sourceApp: 2-32 lowercase alphanumeric characters or hyphens
|
|
47
|
+
// - entityType: one of the supported entity types
|
|
48
|
+
// - uuid: UUID v4 format (36 characters with hyphens)
|
|
49
|
+
const EXTERNAL_ID_REGEX =
|
|
50
|
+
/^[a-z0-9-]{2,32}:(objective|keyResult|risk|initiative|indicator|indicatorValue|indicatorForecast|milestone|team|company|user):[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
51
|
+
|
|
52
|
+
// Simpler regex for basic validation (allows any UUID-like format)
|
|
53
|
+
const EXTERNAL_ID_REGEX_SIMPLE =
|
|
54
|
+
/^[a-z0-9-]{2,32}:(objective|keyResult|risk|initiative|indicator|indicatorValue|indicatorForecast|milestone|team|company|user):[a-f0-9-]{36}$/;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validates an external ID format
|
|
58
|
+
* @param id - The external ID to validate
|
|
59
|
+
* @returns true if the ID is valid, false otherwise
|
|
60
|
+
*/
|
|
61
|
+
export function validateExternalId(id: string): boolean {
|
|
62
|
+
return EXTERNAL_ID_REGEX_SIMPLE.test(id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validates a source app name format
|
|
67
|
+
* @param sourceApp - The source app name to validate
|
|
68
|
+
* @returns true if valid, false otherwise
|
|
69
|
+
*/
|
|
70
|
+
export function validateSourceApp(sourceApp: string): boolean {
|
|
71
|
+
return /^[a-z0-9-]{2,32}$/.test(sourceApp);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generates a new external ID for the given source app and entity type
|
|
76
|
+
* @param sourceApp - The identifier of the source application (2-32 lowercase alphanumeric or hyphens)
|
|
77
|
+
* @param entityType - The type of entity (objective, keyResult, risk, initiative, indicator, milestone)
|
|
78
|
+
* @returns A new external ID in the format {sourceApp}:{entityType}:{uuid}
|
|
79
|
+
* @throws Error if sourceApp format is invalid
|
|
80
|
+
*/
|
|
81
|
+
export function generateExternalId(
|
|
82
|
+
sourceApp: string,
|
|
83
|
+
entityType: EntityType
|
|
84
|
+
): string {
|
|
85
|
+
if (!validateSourceApp(sourceApp)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Invalid sourceApp format: "${sourceApp}". Must be 2-32 lowercase alphanumeric characters or hyphens.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const uuid = crypto.randomUUID();
|
|
92
|
+
return `${sourceApp}:${entityType}:${uuid}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parsed external ID structure
|
|
97
|
+
*/
|
|
98
|
+
export interface ParsedExternalId {
|
|
99
|
+
sourceApp: string;
|
|
100
|
+
entityType: EntityType;
|
|
101
|
+
uuid: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parses an external ID into its components
|
|
106
|
+
* @param id - The external ID to parse
|
|
107
|
+
* @returns The parsed components, or null if the ID is invalid
|
|
108
|
+
*/
|
|
109
|
+
export function parseExternalId(id: string): ParsedExternalId | null {
|
|
110
|
+
const match = id.match(
|
|
111
|
+
/^([a-z0-9-]{2,32}):(objective|keyResult|risk|initiative|indicator|indicatorValue|indicatorForecast|milestone|team|company|user):([a-f0-9-]{36})$/
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (!match) return null;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
sourceApp: match[1],
|
|
118
|
+
entityType: match[2] as EntityType,
|
|
119
|
+
uuid: match[3],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extracts the source app from an external ID
|
|
125
|
+
* @param id - The external ID
|
|
126
|
+
* @returns The source app, or null if invalid
|
|
127
|
+
*/
|
|
128
|
+
export function extractSourceApp(id: string): string | null {
|
|
129
|
+
const parsed = parseExternalId(id);
|
|
130
|
+
return parsed?.sourceApp ?? null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extracts the entity type from an external ID
|
|
135
|
+
* @param id - The external ID
|
|
136
|
+
* @returns The entity type, or null if invalid
|
|
137
|
+
*/
|
|
138
|
+
export function extractEntityType(id: string): EntityType | null {
|
|
139
|
+
const parsed = parseExternalId(id);
|
|
140
|
+
return parsed?.entityType ?? null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Checks if two external IDs belong to the same source app
|
|
145
|
+
* @param id1 - First external ID
|
|
146
|
+
* @param id2 - Second external ID
|
|
147
|
+
* @returns true if both IDs are from the same source app
|
|
148
|
+
*/
|
|
149
|
+
export function sameSourceApp(id1: string, id2: string): boolean {
|
|
150
|
+
const app1 = extractSourceApp(id1);
|
|
151
|
+
const app2 = extractSourceApp(id2);
|
|
152
|
+
return app1 !== null && app1 === app2;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Convex validator for external ID (as string with runtime validation)
|
|
157
|
+
* Use this in your mutation/query args
|
|
158
|
+
*/
|
|
159
|
+
export const externalIdValidator = v.string();
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Version information for the OKRHub component
|
|
163
|
+
* Used for compatibility checks with LinkHub API
|
|
164
|
+
*/
|
|
165
|
+
export const OKRHUB_VERSION = "0.1.0";
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Gets the current component version
|
|
169
|
+
*/
|
|
170
|
+
export function getVersion(): string {
|
|
171
|
+
return OKRHUB_VERSION;
|
|
172
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC Signature Utilities for OKRHub Component
|
|
3
|
+
*
|
|
4
|
+
* Provides cryptographic signature functions for secure API communication
|
|
5
|
+
* with LinkHub's ingest endpoints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { OKRHUB_VERSION } from "../externalId.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates HMAC-SHA256 signature for payload authentication
|
|
12
|
+
*
|
|
13
|
+
* SECURITY NOTE: Uses the signing secret (not the API key) to create
|
|
14
|
+
* cryptographically consistent signatures that the server can verify.
|
|
15
|
+
*/
|
|
16
|
+
export async function createHmacSignature(
|
|
17
|
+
payload: string,
|
|
18
|
+
signingSecret: string
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
const keyData = encoder.encode(signingSecret);
|
|
22
|
+
const messageData = encoder.encode(payload);
|
|
23
|
+
|
|
24
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
25
|
+
"raw",
|
|
26
|
+
keyData,
|
|
27
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
28
|
+
false,
|
|
29
|
+
["sign"]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
|
|
33
|
+
const hashArray = Array.from(new Uint8Array(signature));
|
|
34
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates request headers with HMAC signature, version, and key prefix
|
|
39
|
+
*/
|
|
40
|
+
export async function createRequestHeaders(
|
|
41
|
+
payload: string,
|
|
42
|
+
apiKeyPrefix: string,
|
|
43
|
+
signingSecret: string
|
|
44
|
+
): Promise<Headers> {
|
|
45
|
+
const signature = await createHmacSignature(payload, signingSecret);
|
|
46
|
+
|
|
47
|
+
return new Headers({
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
"X-OKRHub-Version": OKRHUB_VERSION,
|
|
50
|
+
"X-OKRHub-Key-Prefix": apiKeyPrefix,
|
|
51
|
+
"X-OKRHub-Signature": signature,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Types for OKRHub Component
|
|
3
|
+
*
|
|
4
|
+
* Response types and shared interfaces used across the component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Response from LinkHub's single entity ingest endpoint
|
|
9
|
+
*/
|
|
10
|
+
export interface IngestResponse {
|
|
11
|
+
success: boolean;
|
|
12
|
+
externalId: string;
|
|
13
|
+
linkHubId?: string;
|
|
14
|
+
action: "create" | "update";
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Response from LinkHub's batch ingest endpoint
|
|
20
|
+
*/
|
|
21
|
+
export interface BatchIngestResponse {
|
|
22
|
+
success: boolean;
|
|
23
|
+
results: {
|
|
24
|
+
entityType: string;
|
|
25
|
+
externalId: string;
|
|
26
|
+
linkHubId?: string;
|
|
27
|
+
action: "create" | "update";
|
|
28
|
+
error?: string;
|
|
29
|
+
}[];
|
|
30
|
+
errors: string[];
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utilities for OKRHub Component
|
|
3
|
+
*
|
|
4
|
+
* Provides validation helpers for external IDs and slug generation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { validateExternalId } from "../externalId.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates external ID format and throws if invalid
|
|
11
|
+
*/
|
|
12
|
+
export function assertValidExternalId(
|
|
13
|
+
externalId: string,
|
|
14
|
+
fieldName = "externalId"
|
|
15
|
+
): void {
|
|
16
|
+
if (!validateExternalId(externalId)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Invalid ${fieldName} format: "${externalId}". ` +
|
|
19
|
+
`Expected format: {sourceApp}:{entityType}:{uuid}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generates a slug from text with sourceApp prefix
|
|
26
|
+
* Pattern: {sourceApp}-{baseSlug}-{suffix}
|
|
27
|
+
*/
|
|
28
|
+
export function generateSlug(
|
|
29
|
+
sourceApp: string,
|
|
30
|
+
text: string,
|
|
31
|
+
maxLength = 50
|
|
32
|
+
): string {
|
|
33
|
+
const baseSlug = text
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
36
|
+
.replace(/^-|-$/g, "")
|
|
37
|
+
.substring(0, maxLength - sourceApp.length - 7); // Reserve space for prefix and suffix
|
|
38
|
+
|
|
39
|
+
const suffix = Math.random().toString(36).substring(2, 6);
|
|
40
|
+
return `${sourceApp}-${baseSlug}-${suffix}`;
|
|
41
|
+
}
|