@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,254 @@
1
+ /**
2
+ * Indicator Values Entity for OKRHub Component
3
+ *
4
+ * CRUD operations and queries for Indicator Values.
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 { assertValidExternalId } from "../lib/validation.js";
11
+ import { indicatorValuePayloadValidator, SyncStatusSchema } from "../schema.js";
12
+
13
+ // ============================================================================
14
+ // LOCAL CRUD MUTATIONS
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Creates an indicator value locally and queues for sync
19
+ */
20
+ export const createIndicatorValue = mutation({
21
+ args: {
22
+ sourceApp: v.string(),
23
+ sourceUrl: v.optional(v.string()),
24
+ indicatorExternalId: v.string(),
25
+ value: v.number(),
26
+ date: v.number(),
27
+ },
28
+ returns: v.object({
29
+ success: v.boolean(),
30
+ externalId: v.string(),
31
+ localId: v.id("indicatorValues"),
32
+ queueId: v.optional(v.id("syncQueue")),
33
+ error: v.optional(v.string()),
34
+ }),
35
+ handler: async (ctx, args) => {
36
+ const { sourceApp, sourceUrl, indicatorExternalId, value, date } = args;
37
+
38
+ try {
39
+ assertValidExternalId(indicatorExternalId, "indicatorExternalId");
40
+
41
+ const uuid = crypto.randomUUID();
42
+ const externalId = `${sourceApp}:indicatorValue:${uuid}`;
43
+ const now = Date.now();
44
+
45
+ const localId = await ctx.db.insert("indicatorValues", {
46
+ externalId,
47
+ indicatorExternalId,
48
+ value,
49
+ date,
50
+ syncStatus: "pending",
51
+ createdAt: now,
52
+ });
53
+
54
+ const payload = JSON.stringify({
55
+ externalId,
56
+ indicatorExternalId,
57
+ value,
58
+ date,
59
+ sourceUrl,
60
+ createdAt: now,
61
+ });
62
+
63
+ const queueId = await ctx.db.insert("syncQueue", {
64
+ entityType: "indicatorValue",
65
+ externalId,
66
+ payload,
67
+ status: "pending",
68
+ attempts: 0,
69
+ createdAt: now,
70
+ });
71
+
72
+ return {
73
+ success: true,
74
+ externalId,
75
+ localId,
76
+ queueId,
77
+ };
78
+ } catch (error) {
79
+ const errorMessage =
80
+ error && typeof error === "object" && "message" in error
81
+ ? (error.message as string)
82
+ : "Unknown error";
83
+
84
+ return {
85
+ success: false,
86
+ externalId: "",
87
+ localId: "" as Id<"indicatorValues">,
88
+ error: errorMessage,
89
+ };
90
+ }
91
+ },
92
+ });
93
+
94
+ // ============================================================================
95
+ // LOCAL QUERY FUNCTIONS
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Gets all local indicator values
100
+ */
101
+ export const getAllIndicatorValues = query({
102
+ args: {},
103
+ returns: v.array(
104
+ v.object({
105
+ _id: v.id("indicatorValues"),
106
+ _creationTime: v.number(),
107
+ externalId: v.string(),
108
+ indicatorExternalId: v.string(),
109
+ value: v.number(),
110
+ date: v.number(),
111
+ syncStatus: SyncStatusSchema,
112
+ createdAt: v.number(),
113
+ })
114
+ ),
115
+ handler: async (ctx) => {
116
+ return await ctx.db.query("indicatorValues").collect();
117
+ },
118
+ });
119
+
120
+ // ============================================================================
121
+ // UPDATE MUTATIONS
122
+ // ============================================================================
123
+
124
+ /**
125
+ * Updates an indicator value locally and queues for sync
126
+ * Resets syncStatus to "pending"
127
+ */
128
+ export const updateIndicatorValue = mutation({
129
+ args: {
130
+ externalId: v.string(),
131
+ value: v.optional(v.number()),
132
+ date: v.optional(v.number()),
133
+ },
134
+ returns: v.object({
135
+ success: v.boolean(),
136
+ externalId: v.string(),
137
+ queueId: v.optional(v.id("syncQueue")),
138
+ error: v.optional(v.string()),
139
+ }),
140
+ handler: async (ctx, args) => {
141
+ const { externalId, value, date } = args;
142
+
143
+ try {
144
+ // Find the indicator value by externalId
145
+ const indicatorValue = await ctx.db
146
+ .query("indicatorValues")
147
+ .withIndex("by_external_id", (q) => q.eq("externalId", externalId))
148
+ .first();
149
+
150
+ if (!indicatorValue) {
151
+ return {
152
+ success: false,
153
+ externalId,
154
+ error: `Indicator value not found: ${externalId}`,
155
+ };
156
+ }
157
+
158
+ const now = Date.now();
159
+
160
+ // Update the indicator value
161
+ await ctx.db.patch(indicatorValue._id, {
162
+ ...(value !== undefined && { value }),
163
+ ...(date !== undefined && { date }),
164
+ syncStatus: "pending",
165
+ });
166
+
167
+ // Create payload for sync with updated values
168
+ const updatedIndicatorValue = {
169
+ externalId,
170
+ indicatorExternalId: indicatorValue.indicatorExternalId,
171
+ value: value ?? indicatorValue.value,
172
+ date: date ?? indicatorValue.date,
173
+ };
174
+
175
+ const payload = JSON.stringify(updatedIndicatorValue);
176
+
177
+ // Add to sync queue
178
+ const queueId = await ctx.db.insert("syncQueue", {
179
+ entityType: "indicatorValue",
180
+ externalId,
181
+ payload,
182
+ status: "pending",
183
+ attempts: 0,
184
+ createdAt: now,
185
+ });
186
+
187
+ return {
188
+ success: true,
189
+ externalId,
190
+ queueId,
191
+ };
192
+ } catch (error) {
193
+ const errorMessage =
194
+ error && typeof error === "object" && "message" in error
195
+ ? (error.message as string)
196
+ : "Unknown error";
197
+
198
+ return {
199
+ success: false,
200
+ externalId,
201
+ error: errorMessage,
202
+ };
203
+ }
204
+ },
205
+ });
206
+
207
+ // ============================================================================
208
+ // PUBLIC MUTATIONS - Entry points for consumers
209
+ // ============================================================================
210
+
211
+ /**
212
+ * Insert an indicator value into LinkHub
213
+ */
214
+ export const insertIndicatorValue = mutation({
215
+ args: {
216
+ indicatorValue: indicatorValuePayloadValidator,
217
+ },
218
+ returns: v.object({
219
+ success: v.boolean(),
220
+ externalId: v.string(),
221
+ queueId: v.optional(v.id("syncQueue")),
222
+ error: v.optional(v.string()),
223
+ }),
224
+ handler: async (ctx, args) => {
225
+ const { indicatorValue } = args;
226
+
227
+ // Validate external IDs
228
+ assertValidExternalId(
229
+ indicatorValue.externalId,
230
+ "indicatorValue.externalId"
231
+ );
232
+ assertValidExternalId(
233
+ indicatorValue.indicatorExternalId,
234
+ "indicatorValue.indicatorExternalId"
235
+ );
236
+
237
+ // Add to sync queue
238
+ const payload = JSON.stringify(indicatorValue);
239
+ const queueId = await ctx.db.insert("syncQueue", {
240
+ entityType: "indicatorValue",
241
+ externalId: indicatorValue.externalId,
242
+ payload,
243
+ status: "pending",
244
+ attempts: 0,
245
+ createdAt: Date.now(),
246
+ });
247
+
248
+ return {
249
+ success: true,
250
+ externalId: indicatorValue.externalId,
251
+ queueId,
252
+ };
253
+ },
254
+ });
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Indicators Entity for OKRHub Component
3
+ *
4
+ * CRUD operations and queries for Indicators.
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
+ indicatorPayloadValidator,
14
+ PeriodicitySchema,
15
+ SyncStatusSchema,
16
+ } from "../schema.js";
17
+
18
+ // ============================================================================
19
+ // LOCAL CRUD MUTATIONS
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Creates an indicator locally and queues for sync
24
+ */
25
+ export const createIndicator = mutation({
26
+ args: {
27
+ sourceApp: v.string(),
28
+ sourceUrl: v.optional(v.string()),
29
+ companyExternalId: v.string(),
30
+ description: v.string(),
31
+ symbol: v.string(),
32
+ periodicity: PeriodicitySchema,
33
+ isReverse: v.optional(v.boolean()),
34
+ },
35
+ returns: v.object({
36
+ success: v.boolean(),
37
+ externalId: v.string(),
38
+ localId: v.id("indicators"),
39
+ queueId: v.optional(v.id("syncQueue")),
40
+ error: v.optional(v.string()),
41
+ }),
42
+ handler: async (ctx, args) => {
43
+ const {
44
+ sourceApp,
45
+ sourceUrl,
46
+ companyExternalId,
47
+ description,
48
+ symbol,
49
+ periodicity,
50
+ isReverse,
51
+ } = args;
52
+
53
+ try {
54
+ assertValidExternalId(companyExternalId, "companyExternalId");
55
+
56
+ const externalId = generateExternalId(sourceApp, "indicator");
57
+ const slug = generateSlug(sourceApp, description);
58
+ const now = Date.now();
59
+
60
+ const localId = await ctx.db.insert("indicators", {
61
+ externalId,
62
+ companyExternalId,
63
+ description,
64
+ symbol,
65
+ periodicity,
66
+ isReverse,
67
+ slug,
68
+ syncStatus: "pending",
69
+ createdAt: now,
70
+ });
71
+
72
+ const payload = JSON.stringify({
73
+ externalId,
74
+ companyExternalId,
75
+ description,
76
+ symbol,
77
+ periodicity,
78
+ isReverse,
79
+ sourceUrl,
80
+ createdAt: now,
81
+ });
82
+
83
+ const queueId = await ctx.db.insert("syncQueue", {
84
+ entityType: "indicator",
85
+ externalId,
86
+ payload,
87
+ status: "pending",
88
+ attempts: 0,
89
+ createdAt: now,
90
+ });
91
+
92
+ return {
93
+ success: true,
94
+ externalId,
95
+ localId,
96
+ queueId,
97
+ };
98
+ } catch (error) {
99
+ const errorMessage =
100
+ error && typeof error === "object" && "message" in error
101
+ ? (error.message as string)
102
+ : "Unknown error";
103
+
104
+ return {
105
+ success: false,
106
+ externalId: "",
107
+ localId: "" as Id<"indicators">,
108
+ error: errorMessage,
109
+ };
110
+ }
111
+ },
112
+ });
113
+
114
+ // ============================================================================
115
+ // LOCAL QUERY FUNCTIONS
116
+ // ============================================================================
117
+
118
+ /**
119
+ * Gets all local indicators
120
+ */
121
+ export const getAllIndicators = query({
122
+ args: {},
123
+ returns: v.array(
124
+ v.object({
125
+ _id: v.id("indicators"),
126
+ _creationTime: v.number(),
127
+ externalId: v.string(),
128
+ companyExternalId: v.string(),
129
+ description: v.string(),
130
+ symbol: v.string(),
131
+ periodicity: PeriodicitySchema,
132
+ isReverse: v.optional(v.boolean()),
133
+ slug: v.string(),
134
+ syncStatus: SyncStatusSchema,
135
+ createdAt: v.number(),
136
+ deletedAt: v.optional(v.number()),
137
+ })
138
+ ),
139
+ handler: async (ctx) => {
140
+ return await ctx.db
141
+ .query("indicators")
142
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
143
+ .collect();
144
+ },
145
+ });
146
+
147
+ // ============================================================================
148
+ // UPDATE MUTATIONS
149
+ // ============================================================================
150
+
151
+ /**
152
+ * Updates an indicator locally and queues for sync
153
+ * Resets syncStatus to "pending"
154
+ */
155
+ export const updateIndicator = mutation({
156
+ args: {
157
+ externalId: v.string(),
158
+ description: v.optional(v.string()),
159
+ symbol: v.optional(v.string()),
160
+ periodicity: v.optional(PeriodicitySchema),
161
+ isReverse: v.optional(v.boolean()),
162
+ },
163
+ returns: v.object({
164
+ success: v.boolean(),
165
+ externalId: v.string(),
166
+ queueId: v.optional(v.id("syncQueue")),
167
+ error: v.optional(v.string()),
168
+ }),
169
+ handler: async (ctx, args) => {
170
+ const {
171
+ externalId,
172
+ description,
173
+ symbol,
174
+ periodicity,
175
+ isReverse,
176
+ } = args;
177
+
178
+ try {
179
+ // Find the indicator by externalId
180
+ const indicator = await ctx.db
181
+ .query("indicators")
182
+ .withIndex("by_external_id", (q) => q.eq("externalId", externalId))
183
+ .first();
184
+
185
+ if (!indicator) {
186
+ return {
187
+ success: false,
188
+ externalId,
189
+ error: `Indicator not found: ${externalId}`,
190
+ };
191
+ }
192
+
193
+ const now = Date.now();
194
+
195
+ // Update the indicator
196
+ await ctx.db.patch(indicator._id, {
197
+ ...(description !== undefined && { description }),
198
+ ...(symbol !== undefined && { symbol }),
199
+ ...(periodicity !== undefined && { periodicity }),
200
+ ...(isReverse !== undefined && { isReverse }),
201
+ syncStatus: "pending",
202
+ });
203
+
204
+ // Create payload for sync with updated values
205
+ const updatedIndicator = {
206
+ externalId,
207
+ companyExternalId: indicator.companyExternalId,
208
+ description: description ?? indicator.description,
209
+ symbol: symbol ?? indicator.symbol,
210
+ periodicity: periodicity ?? indicator.periodicity,
211
+ isReverse: isReverse ?? indicator.isReverse,
212
+ };
213
+
214
+ const payload = JSON.stringify(updatedIndicator);
215
+
216
+ // Add to sync queue
217
+ const queueId = await ctx.db.insert("syncQueue", {
218
+ entityType: "indicator",
219
+ externalId,
220
+ payload,
221
+ status: "pending",
222
+ attempts: 0,
223
+ createdAt: now,
224
+ });
225
+
226
+ return {
227
+ success: true,
228
+ externalId,
229
+ queueId,
230
+ };
231
+ } catch (error) {
232
+ const errorMessage =
233
+ error && typeof error === "object" && "message" in error
234
+ ? (error.message as string)
235
+ : "Unknown error";
236
+
237
+ return {
238
+ success: false,
239
+ externalId,
240
+ error: errorMessage,
241
+ };
242
+ }
243
+ },
244
+ });
245
+
246
+ // ============================================================================
247
+ // PUBLIC MUTATIONS - Entry points for consumers
248
+ // ============================================================================
249
+
250
+ /**
251
+ * Insert an indicator into LinkHub
252
+ */
253
+ export const insertIndicator = mutation({
254
+ args: {
255
+ indicator: indicatorPayloadValidator,
256
+ },
257
+ returns: v.object({
258
+ success: v.boolean(),
259
+ externalId: v.string(),
260
+ queueId: v.optional(v.id("syncQueue")),
261
+ error: v.optional(v.string()),
262
+ }),
263
+ handler: async (ctx, args) => {
264
+ const { indicator } = args;
265
+
266
+ // Validate external IDs
267
+ assertValidExternalId(indicator.externalId, "indicator.externalId");
268
+ assertValidExternalId(
269
+ indicator.companyExternalId,
270
+ "indicator.companyExternalId"
271
+ );
272
+
273
+ // Add to sync queue
274
+ const payload = JSON.stringify(indicator);
275
+ const queueId = await ctx.db.insert("syncQueue", {
276
+ entityType: "indicator",
277
+ externalId: indicator.externalId,
278
+ payload,
279
+ status: "pending",
280
+ attempts: 0,
281
+ createdAt: Date.now(),
282
+ });
283
+
284
+ return {
285
+ success: true,
286
+ externalId: indicator.externalId,
287
+ queueId,
288
+ };
289
+ },
290
+ });