@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.
Files changed (148) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +579 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +593 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +704 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +72 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +1986 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +3 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/entities/batch.d.ts +121 -0
  32. package/dist/component/entities/batch.d.ts.map +1 -0
  33. package/dist/component/entities/batch.js +81 -0
  34. package/dist/component/entities/batch.js.map +1 -0
  35. package/dist/component/entities/index.d.ts +13 -0
  36. package/dist/component/entities/index.d.ts.map +1 -0
  37. package/dist/component/entities/index.js +22 -0
  38. package/dist/component/entities/index.js.map +1 -0
  39. package/dist/component/entities/indicatorForecasts.d.ts +61 -0
  40. package/dist/component/entities/indicatorForecasts.d.ts.map +1 -0
  41. package/dist/component/entities/indicatorForecasts.js +180 -0
  42. package/dist/component/entities/indicatorForecasts.js.map +1 -0
  43. package/dist/component/entities/indicatorValues.d.ts +77 -0
  44. package/dist/component/entities/indicatorValues.d.ts.map +1 -0
  45. package/dist/component/entities/indicatorValues.js +218 -0
  46. package/dist/component/entities/indicatorValues.js.map +1 -0
  47. package/dist/component/entities/indicators.d.ts +90 -0
  48. package/dist/component/entities/indicators.d.ts.map +1 -0
  49. package/dist/component/entities/indicators.js +239 -0
  50. package/dist/component/entities/indicators.js.map +1 -0
  51. package/dist/component/entities/initiatives.d.ts +103 -0
  52. package/dist/component/entities/initiatives.d.ts.map +1 -0
  53. package/dist/component/entities/initiatives.js +275 -0
  54. package/dist/component/entities/initiatives.js.map +1 -0
  55. package/dist/component/entities/keyResults.d.ts +111 -0
  56. package/dist/component/entities/keyResults.d.ts.map +1 -0
  57. package/dist/component/entities/keyResults.js +284 -0
  58. package/dist/component/entities/keyResults.js.map +1 -0
  59. package/dist/component/entities/milestones.d.ts +93 -0
  60. package/dist/component/entities/milestones.d.ts.map +1 -0
  61. package/dist/component/entities/milestones.js +249 -0
  62. package/dist/component/entities/milestones.js.map +1 -0
  63. package/dist/component/entities/objectives.d.ts +99 -0
  64. package/dist/component/entities/objectives.d.ts.map +1 -0
  65. package/dist/component/entities/objectives.js +261 -0
  66. package/dist/component/entities/objectives.js.map +1 -0
  67. package/dist/component/entities/risks.d.ts +126 -0
  68. package/dist/component/entities/risks.d.ts.map +1 -0
  69. package/dist/component/entities/risks.js +315 -0
  70. package/dist/component/entities/risks.js.map +1 -0
  71. package/dist/component/externalId.d.ts +79 -0
  72. package/dist/component/externalId.d.ts.map +1 -0
  73. package/dist/component/externalId.js +124 -0
  74. package/dist/component/externalId.js.map +1 -0
  75. package/dist/component/lib/hmac.d.ts +18 -0
  76. package/dist/component/lib/hmac.d.ts.map +1 -0
  77. package/dist/component/lib/hmac.js +35 -0
  78. package/dist/component/lib/hmac.js.map +1 -0
  79. package/dist/component/lib/index.d.ts +7 -0
  80. package/dist/component/lib/index.d.ts.map +1 -0
  81. package/dist/component/lib/index.js +6 -0
  82. package/dist/component/lib/index.js.map +1 -0
  83. package/dist/component/lib/types.d.ts +30 -0
  84. package/dist/component/lib/types.d.ts.map +1 -0
  85. package/dist/component/lib/types.js +7 -0
  86. package/dist/component/lib/types.js.map +1 -0
  87. package/dist/component/lib/validation.d.ts +15 -0
  88. package/dist/component/lib/validation.d.ts.map +1 -0
  89. package/dist/component/lib/validation.js +29 -0
  90. package/dist/component/lib/validation.js.map +1 -0
  91. package/dist/component/okrhub.d.ts +31 -0
  92. package/dist/component/okrhub.d.ts.map +1 -0
  93. package/dist/component/okrhub.js +45 -0
  94. package/dist/component/okrhub.js.map +1 -0
  95. package/dist/component/schema.d.ts +943 -0
  96. package/dist/component/schema.d.ts.map +1 -0
  97. package/dist/component/schema.js +437 -0
  98. package/dist/component/schema.js.map +1 -0
  99. package/dist/component/sync/http.d.ts +30 -0
  100. package/dist/component/sync/http.d.ts.map +1 -0
  101. package/dist/component/sync/http.js +114 -0
  102. package/dist/component/sync/http.js.map +1 -0
  103. package/dist/component/sync/index.d.ts +7 -0
  104. package/dist/component/sync/index.d.ts.map +1 -0
  105. package/dist/component/sync/index.js +7 -0
  106. package/dist/component/sync/index.js.map +1 -0
  107. package/dist/component/sync/processor.d.ts +20 -0
  108. package/dist/component/sync/processor.d.ts.map +1 -0
  109. package/dist/component/sync/processor.js +67 -0
  110. package/dist/component/sync/processor.js.map +1 -0
  111. package/dist/component/sync/queue.d.ts +40 -0
  112. package/dist/component/sync/queue.d.ts.map +1 -0
  113. package/dist/component/sync/queue.js +176 -0
  114. package/dist/component/sync/queue.js.map +1 -0
  115. package/dist/react/index.d.ts +2 -0
  116. package/dist/react/index.d.ts.map +1 -0
  117. package/dist/react/index.js +6 -0
  118. package/dist/react/index.js.map +1 -0
  119. package/package.json +117 -0
  120. package/src/client/_generated/_ignore.ts +1 -0
  121. package/src/client/index.ts +1004 -0
  122. package/src/component/_generated/api.ts +88 -0
  123. package/src/component/_generated/component.ts +2685 -0
  124. package/src/component/_generated/dataModel.ts +60 -0
  125. package/src/component/_generated/server.ts +156 -0
  126. package/src/component/convex.config.ts +3 -0
  127. package/src/component/entities/batch.ts +90 -0
  128. package/src/component/entities/index.ts +64 -0
  129. package/src/component/entities/indicatorForecasts.ts +205 -0
  130. package/src/component/entities/indicatorValues.ts +254 -0
  131. package/src/component/entities/indicators.ts +290 -0
  132. package/src/component/entities/initiatives.ts +342 -0
  133. package/src/component/entities/keyResults.ts +334 -0
  134. package/src/component/entities/milestones.ts +296 -0
  135. package/src/component/entities/objectives.ts +294 -0
  136. package/src/component/entities/risks.ts +383 -0
  137. package/src/component/externalId.ts +172 -0
  138. package/src/component/lib/hmac.ts +53 -0
  139. package/src/component/lib/index.ts +7 -0
  140. package/src/component/lib/types.ts +31 -0
  141. package/src/component/lib/validation.ts +41 -0
  142. package/src/component/okrhub.ts +110 -0
  143. package/src/component/schema.ts +574 -0
  144. package/src/component/sync/http.ts +138 -0
  145. package/src/component/sync/index.ts +11 -0
  146. package/src/component/sync/processor.ts +77 -0
  147. package/src/component/sync/queue.ts +201 -0
  148. package/src/react/index.ts +7 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Milestones Entity for OKRHub Component
3
+ *
4
+ * CRUD operations and queries for Milestones.
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
+ milestonePayloadValidator,
14
+ MilestoneStatusSchema,
15
+ SyncStatusSchema,
16
+ } from "../schema.js";
17
+
18
+ // ============================================================================
19
+ // LOCAL CRUD MUTATIONS
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Creates a milestone locally and queues for sync
24
+ */
25
+ export const createMilestone = mutation({
26
+ args: {
27
+ sourceApp: v.string(),
28
+ sourceUrl: v.optional(v.string()),
29
+ indicatorExternalId: v.string(),
30
+ description: v.string(),
31
+ value: v.number(),
32
+ forecastDate: v.optional(v.number()),
33
+ status: v.optional(MilestoneStatusSchema),
34
+ achievedAt: v.optional(v.number()),
35
+ },
36
+ returns: v.object({
37
+ success: v.boolean(),
38
+ externalId: v.string(),
39
+ localId: v.id("milestones"),
40
+ queueId: v.optional(v.id("syncQueue")),
41
+ error: v.optional(v.string()),
42
+ }),
43
+ handler: async (ctx, args) => {
44
+ const {
45
+ sourceApp,
46
+ sourceUrl,
47
+ indicatorExternalId,
48
+ description,
49
+ value,
50
+ forecastDate,
51
+ status,
52
+ achievedAt,
53
+ } = args;
54
+
55
+ try {
56
+ assertValidExternalId(indicatorExternalId, "indicatorExternalId");
57
+
58
+ const externalId = generateExternalId(sourceApp, "milestone");
59
+ const slug = generateSlug(sourceApp, description);
60
+ const now = Date.now();
61
+
62
+ const localId = await ctx.db.insert("milestones", {
63
+ externalId,
64
+ indicatorExternalId,
65
+ description,
66
+ value,
67
+ forecastDate,
68
+ status: status ?? "ON_TIME", // Default to ON_TIME
69
+ achievedAt,
70
+ slug,
71
+ syncStatus: "pending",
72
+ createdAt: now,
73
+ });
74
+
75
+ const payload = JSON.stringify({
76
+ externalId,
77
+ indicatorExternalId,
78
+ description,
79
+ value,
80
+ forecastDate,
81
+ status: status ?? "ON_TIME", // Default to ON_TIME
82
+ achievedAt,
83
+ sourceUrl,
84
+ createdAt: now,
85
+ });
86
+
87
+ const queueId = await ctx.db.insert("syncQueue", {
88
+ entityType: "milestone",
89
+ externalId,
90
+ payload,
91
+ status: "pending",
92
+ attempts: 0,
93
+ createdAt: now,
94
+ });
95
+
96
+ return {
97
+ success: true,
98
+ externalId,
99
+ localId,
100
+ queueId,
101
+ };
102
+ } catch (error) {
103
+ const errorMessage =
104
+ error && typeof error === "object" && "message" in error
105
+ ? (error.message as string)
106
+ : "Unknown error";
107
+
108
+ return {
109
+ success: false,
110
+ externalId: "",
111
+ localId: "" as Id<"milestones">,
112
+ error: errorMessage,
113
+ };
114
+ }
115
+ },
116
+ });
117
+
118
+ // ============================================================================
119
+ // LOCAL QUERY FUNCTIONS
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Gets all local milestones
124
+ */
125
+ export const getAllMilestones = query({
126
+ args: {},
127
+ returns: v.array(
128
+ v.object({
129
+ _id: v.id("milestones"),
130
+ _creationTime: v.number(),
131
+ externalId: v.string(),
132
+ indicatorExternalId: v.string(),
133
+ description: v.string(),
134
+ value: v.number(),
135
+ forecastDate: v.optional(v.number()),
136
+ status: MilestoneStatusSchema, // Required
137
+ achievedAt: v.optional(v.number()),
138
+ slug: v.string(),
139
+ syncStatus: SyncStatusSchema,
140
+ createdAt: v.number(),
141
+ updatedAt: v.optional(v.number()),
142
+ deletedAt: v.optional(v.number()),
143
+ })
144
+ ),
145
+ handler: async (ctx) => {
146
+ return await ctx.db
147
+ .query("milestones")
148
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
149
+ .collect();
150
+ },
151
+ });
152
+
153
+ // ============================================================================
154
+ // UPDATE MUTATIONS
155
+ // ============================================================================
156
+
157
+ /**
158
+ * Updates a milestone locally and queues for sync
159
+ * Resets syncStatus to "pending"
160
+ */
161
+ export const updateMilestone = mutation({
162
+ args: {
163
+ externalId: v.string(),
164
+ description: v.optional(v.string()),
165
+ value: v.optional(v.number()),
166
+ forecastDate: v.optional(v.number()),
167
+ status: v.optional(MilestoneStatusSchema),
168
+ achievedAt: v.optional(v.number()),
169
+ },
170
+ returns: v.object({
171
+ success: v.boolean(),
172
+ externalId: v.string(),
173
+ queueId: v.optional(v.id("syncQueue")),
174
+ error: v.optional(v.string()),
175
+ }),
176
+ handler: async (ctx, args) => {
177
+ const { externalId, description, value, forecastDate, status, achievedAt } =
178
+ args;
179
+
180
+ try {
181
+ // Find the milestone by externalId
182
+ const milestone = await ctx.db
183
+ .query("milestones")
184
+ .withIndex("by_external_id", (q) => q.eq("externalId", externalId))
185
+ .first();
186
+
187
+ if (!milestone) {
188
+ return {
189
+ success: false,
190
+ externalId,
191
+ error: `Milestone not found: ${externalId}`,
192
+ };
193
+ }
194
+
195
+ const now = Date.now();
196
+
197
+ // Update the milestone
198
+ await ctx.db.patch(milestone._id, {
199
+ ...(description !== undefined && { description }),
200
+ ...(value !== undefined && { value }),
201
+ ...(forecastDate !== undefined && { forecastDate }),
202
+ ...(status !== undefined && { status }),
203
+ ...(achievedAt !== undefined && { achievedAt }),
204
+ syncStatus: "pending",
205
+ updatedAt: now,
206
+ });
207
+
208
+ // Create payload for sync with updated values
209
+ const updatedMilestone = {
210
+ externalId,
211
+ indicatorExternalId: milestone.indicatorExternalId,
212
+ description: description ?? milestone.description,
213
+ value: value ?? milestone.value,
214
+ forecastDate: forecastDate ?? milestone.forecastDate,
215
+ status: status ?? milestone.status,
216
+ achievedAt: achievedAt ?? milestone.achievedAt,
217
+ updatedAt: now,
218
+ };
219
+
220
+ const payload = JSON.stringify(updatedMilestone);
221
+
222
+ // Add to sync queue
223
+ const queueId = await ctx.db.insert("syncQueue", {
224
+ entityType: "milestone",
225
+ externalId,
226
+ payload,
227
+ status: "pending",
228
+ attempts: 0,
229
+ createdAt: now,
230
+ });
231
+
232
+ return {
233
+ success: true,
234
+ externalId,
235
+ queueId,
236
+ };
237
+ } catch (error) {
238
+ const errorMessage =
239
+ error && typeof error === "object" && "message" in error
240
+ ? (error.message as string)
241
+ : "Unknown error";
242
+
243
+ return {
244
+ success: false,
245
+ externalId,
246
+ error: errorMessage,
247
+ };
248
+ }
249
+ },
250
+ });
251
+
252
+ // ============================================================================
253
+ // PUBLIC MUTATIONS - Entry points for consumers
254
+ // ============================================================================
255
+
256
+ /**
257
+ * Insert a milestone into LinkHub
258
+ */
259
+ export const insertMilestone = mutation({
260
+ args: {
261
+ milestone: milestonePayloadValidator,
262
+ },
263
+ returns: v.object({
264
+ success: v.boolean(),
265
+ externalId: v.string(),
266
+ queueId: v.optional(v.id("syncQueue")),
267
+ error: v.optional(v.string()),
268
+ }),
269
+ handler: async (ctx, args) => {
270
+ const { milestone } = args;
271
+
272
+ // Validate external IDs
273
+ assertValidExternalId(milestone.externalId, "milestone.externalId");
274
+ assertValidExternalId(
275
+ milestone.indicatorExternalId,
276
+ "milestone.indicatorExternalId"
277
+ );
278
+
279
+ // Add to sync queue
280
+ const payload = JSON.stringify(milestone);
281
+ const queueId = await ctx.db.insert("syncQueue", {
282
+ entityType: "milestone",
283
+ externalId: milestone.externalId,
284
+ payload,
285
+ status: "pending",
286
+ attempts: 0,
287
+ createdAt: Date.now(),
288
+ });
289
+
290
+ return {
291
+ success: true,
292
+ externalId: milestone.externalId,
293
+ queueId,
294
+ };
295
+ },
296
+ });
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Objectives Entity for OKRHub Component
3
+ *
4
+ * CRUD operations and queries for Objectives.
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 { objectivePayloadValidator, SyncStatusSchema } from "../schema.js";
13
+
14
+ // ============================================================================
15
+ // LOCAL CRUD MUTATIONS
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Creates an objective locally and queues for sync
20
+ */
21
+ export const createObjective = mutation({
22
+ args: {
23
+ sourceApp: v.string(),
24
+ sourceUrl: v.optional(v.string()),
25
+ title: v.string(),
26
+ description: v.string(),
27
+ teamExternalId: v.string(),
28
+ },
29
+ returns: v.object({
30
+ success: v.boolean(),
31
+ externalId: v.string(),
32
+ localId: v.id("objectives"),
33
+ queueId: v.optional(v.id("syncQueue")),
34
+ error: v.optional(v.string()),
35
+ }),
36
+ handler: async (ctx, args) => {
37
+ const { sourceApp, sourceUrl, title, description, teamExternalId } = args;
38
+
39
+ try {
40
+ // Validate team external ID
41
+ assertValidExternalId(teamExternalId, "teamExternalId");
42
+
43
+ // Generate external ID for this objective
44
+ const externalId = generateExternalId(sourceApp, "objective");
45
+ const slug = generateSlug(sourceApp, title);
46
+ const now = Date.now();
47
+
48
+ // Save locally
49
+ const localId = await ctx.db.insert("objectives", {
50
+ externalId,
51
+ title,
52
+ description,
53
+ teamExternalId,
54
+ slug,
55
+ syncStatus: "pending",
56
+ createdAt: now,
57
+ });
58
+
59
+ // Create payload for sync
60
+ const payload = JSON.stringify({
61
+ externalId,
62
+ title,
63
+ description,
64
+ teamExternalId,
65
+ sourceUrl,
66
+ createdAt: now,
67
+ });
68
+
69
+ // Add to sync queue
70
+ const queueId = await ctx.db.insert("syncQueue", {
71
+ entityType: "objective",
72
+ externalId,
73
+ payload,
74
+ status: "pending",
75
+ attempts: 0,
76
+ createdAt: now,
77
+ });
78
+
79
+ return {
80
+ success: true,
81
+ externalId,
82
+ localId,
83
+ queueId,
84
+ };
85
+ } catch (error) {
86
+ const errorMessage =
87
+ error && typeof error === "object" && "message" in error
88
+ ? (error.message as string)
89
+ : "Unknown error";
90
+
91
+ return {
92
+ success: false,
93
+ externalId: "",
94
+ localId: "" as Id<"objectives">,
95
+ error: errorMessage,
96
+ };
97
+ }
98
+ },
99
+ });
100
+
101
+ // ============================================================================
102
+ // LOCAL QUERY FUNCTIONS
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Gets all local objectives for a team
107
+ */
108
+ export const getObjectivesByTeam = query({
109
+ args: {
110
+ teamExternalId: v.string(),
111
+ },
112
+ returns: v.array(
113
+ v.object({
114
+ _id: v.id("objectives"),
115
+ _creationTime: v.number(),
116
+ externalId: v.string(),
117
+ title: v.string(),
118
+ description: v.string(),
119
+ teamExternalId: v.string(),
120
+ slug: v.string(),
121
+ syncStatus: SyncStatusSchema,
122
+ createdAt: v.number(),
123
+ updatedAt: v.optional(v.number()),
124
+ deletedAt: v.optional(v.number()),
125
+ })
126
+ ),
127
+ handler: async (ctx, args) => {
128
+ return await ctx.db
129
+ .query("objectives")
130
+ .withIndex("by_team", (q) => q.eq("teamExternalId", args.teamExternalId))
131
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
132
+ .collect();
133
+ },
134
+ });
135
+
136
+ /**
137
+ * Gets all local objectives
138
+ */
139
+ export const getAllObjectives = query({
140
+ args: {},
141
+ returns: v.array(
142
+ v.object({
143
+ _id: v.id("objectives"),
144
+ _creationTime: v.number(),
145
+ externalId: v.string(),
146
+ title: v.string(),
147
+ description: v.string(),
148
+ teamExternalId: v.string(),
149
+ slug: v.string(),
150
+ syncStatus: SyncStatusSchema,
151
+ createdAt: v.number(),
152
+ updatedAt: v.optional(v.number()),
153
+ deletedAt: v.optional(v.number()),
154
+ })
155
+ ),
156
+ handler: async (ctx) => {
157
+ return await ctx.db
158
+ .query("objectives")
159
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
160
+ .collect();
161
+ },
162
+ });
163
+
164
+ // ============================================================================
165
+ // UPDATE MUTATIONS
166
+ // ============================================================================
167
+
168
+ /**
169
+ * Updates an objective locally and queues for sync
170
+ * Resets syncStatus to "pending"
171
+ */
172
+ export const updateObjective = mutation({
173
+ args: {
174
+ externalId: v.string(),
175
+ title: v.optional(v.string()),
176
+ description: v.optional(v.string()),
177
+ },
178
+ returns: v.object({
179
+ success: v.boolean(),
180
+ externalId: v.string(),
181
+ queueId: v.optional(v.id("syncQueue")),
182
+ error: v.optional(v.string()),
183
+ }),
184
+ handler: async (ctx, args) => {
185
+ const { externalId, title, description } = args;
186
+
187
+ try {
188
+ // Find the objective by externalId
189
+ const objective = await ctx.db
190
+ .query("objectives")
191
+ .withIndex("by_external_id", (q) => q.eq("externalId", externalId))
192
+ .first();
193
+
194
+ if (!objective) {
195
+ return {
196
+ success: false,
197
+ externalId,
198
+ error: `Objective not found: ${externalId}`,
199
+ };
200
+ }
201
+
202
+ const now = Date.now();
203
+
204
+ // Update the objective
205
+ await ctx.db.patch(objective._id, {
206
+ ...(title !== undefined && { title }),
207
+ ...(description !== undefined && { description }),
208
+ syncStatus: "pending",
209
+ updatedAt: now,
210
+ });
211
+
212
+ // Create payload for sync with updated values
213
+ const updatedObjective = {
214
+ externalId,
215
+ title: title ?? objective.title,
216
+ description: description ?? objective.description,
217
+ teamExternalId: objective.teamExternalId,
218
+ updatedAt: now,
219
+ };
220
+
221
+ const payload = JSON.stringify(updatedObjective);
222
+
223
+ // Add to sync queue
224
+ const queueId = await ctx.db.insert("syncQueue", {
225
+ entityType: "objective",
226
+ externalId,
227
+ payload,
228
+ status: "pending",
229
+ attempts: 0,
230
+ createdAt: now,
231
+ });
232
+
233
+ return {
234
+ success: true,
235
+ externalId,
236
+ queueId,
237
+ };
238
+ } catch (error) {
239
+ const errorMessage =
240
+ error && typeof error === "object" && "message" in error
241
+ ? (error.message as string)
242
+ : "Unknown error";
243
+
244
+ return {
245
+ success: false,
246
+ externalId,
247
+ error: errorMessage,
248
+ };
249
+ }
250
+ },
251
+ });
252
+
253
+ // ============================================================================
254
+ // PUBLIC MUTATIONS - Entry points for consumers
255
+ // ============================================================================
256
+
257
+ /**
258
+ * Insert an objective into LinkHub
259
+ */
260
+ export const insertObjective = mutation({
261
+ args: {
262
+ objective: objectivePayloadValidator,
263
+ },
264
+ returns: v.object({
265
+ success: v.boolean(),
266
+ externalId: v.string(),
267
+ queueId: v.optional(v.id("syncQueue")),
268
+ error: v.optional(v.string()),
269
+ }),
270
+ handler: async (ctx, args) => {
271
+ const { objective } = args;
272
+
273
+ // Validate external IDs
274
+ assertValidExternalId(objective.externalId, "objective.externalId");
275
+ assertValidExternalId(objective.teamExternalId, "objective.teamExternalId");
276
+
277
+ // Add to sync queue
278
+ const payload = JSON.stringify(objective);
279
+ const queueId = await ctx.db.insert("syncQueue", {
280
+ entityType: "objective",
281
+ externalId: objective.externalId,
282
+ payload,
283
+ status: "pending",
284
+ attempts: 0,
285
+ createdAt: Date.now(),
286
+ });
287
+
288
+ return {
289
+ success: true,
290
+ externalId: objective.externalId,
291
+ queueId,
292
+ };
293
+ },
294
+ });