@fatagnus/dink-convex 1.0.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 +190 -0
- package/README.md +282 -0
- package/convex/convex.config.ts +23 -0
- package/convex/crons.ts +37 -0
- package/convex/http.ts +421 -0
- package/convex/index.ts +20 -0
- package/convex/install.ts +172 -0
- package/convex/outbox.ts +198 -0
- package/convex/outboxProcessor.ts +240 -0
- package/convex/schema.ts +97 -0
- package/convex/sync.ts +327 -0
- package/dist/component.d.ts +34 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/component.js +35 -0
- package/dist/component.js.map +1 -0
- package/dist/crdt.d.ts +82 -0
- package/dist/crdt.d.ts.map +1 -0
- package/dist/crdt.js +134 -0
- package/dist/crdt.js.map +1 -0
- package/dist/factories.d.ts +80 -0
- package/dist/factories.d.ts.map +1 -0
- package/dist/factories.js +159 -0
- package/dist/factories.js.map +1 -0
- package/dist/http.d.ts +238 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +222 -0
- package/dist/http.js.map +1 -0
- package/dist/httpFactory.d.ts +39 -0
- package/dist/httpFactory.d.ts.map +1 -0
- package/dist/httpFactory.js +128 -0
- package/dist/httpFactory.js.map +1 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +217 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +195 -0
- package/dist/schema.js.map +1 -0
- package/dist/syncFactories.d.ts +240 -0
- package/dist/syncFactories.d.ts.map +1 -0
- package/dist/syncFactories.js +623 -0
- package/dist/syncFactories.js.map +1 -0
- package/dist/triggers.d.ts +442 -0
- package/dist/triggers.d.ts.map +1 -0
- package/dist/triggers.js +705 -0
- package/dist/triggers.js.map +1 -0
- package/package.json +108 -0
- package/scripts/check-peer-deps.cjs +132 -0
package/convex/http.ts
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP endpoints for dinkd communication.
|
|
3
|
+
*
|
|
4
|
+
* Provides the /api/dink/applyDelta and /api/dink/getSnapshot endpoints
|
|
5
|
+
* for bidirectional sync with edge devices via dinkd.
|
|
6
|
+
*
|
|
7
|
+
* @module convex/http
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { httpRouter } from "convex/server";
|
|
11
|
+
import { httpAction } from "./_generated/server";
|
|
12
|
+
import { api } from "./_generated/api";
|
|
13
|
+
|
|
14
|
+
const http = httpRouter();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* POST /api/dink/applyDelta
|
|
18
|
+
*
|
|
19
|
+
* Receives CRDT deltas from edge devices and merges them into
|
|
20
|
+
* the target user table. Skips outbox queueing to prevent sync loops.
|
|
21
|
+
*
|
|
22
|
+
* Request:
|
|
23
|
+
* - Authorization: Bearer <appSyncKey>
|
|
24
|
+
* - Body: { collection: string, docId: string, bytes: number[] }
|
|
25
|
+
*
|
|
26
|
+
* Response:
|
|
27
|
+
* - 200: { success: true, seq: number }
|
|
28
|
+
* - 400: { success: false, error: string } - Invalid payload
|
|
29
|
+
* - 401: { success: false, error: string } - Unauthorized
|
|
30
|
+
* - 500: { success: false, error: string } - Server error
|
|
31
|
+
*/
|
|
32
|
+
http.route({
|
|
33
|
+
path: "/api/dink/applyDelta",
|
|
34
|
+
method: "POST",
|
|
35
|
+
handler: httpAction(async (ctx, request) => {
|
|
36
|
+
// Validate Authorization header
|
|
37
|
+
const authHeader = request.headers.get("Authorization");
|
|
38
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
39
|
+
return new Response(
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
success: false,
|
|
42
|
+
error: "Missing or invalid Authorization header",
|
|
43
|
+
}),
|
|
44
|
+
{
|
|
45
|
+
status: 401,
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const token = authHeader.slice(7).trim();
|
|
52
|
+
if (!token) {
|
|
53
|
+
return new Response(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
success: false,
|
|
56
|
+
error: "Missing authorization token",
|
|
57
|
+
}),
|
|
58
|
+
{
|
|
59
|
+
status: 401,
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parse request body
|
|
66
|
+
let payload: { collection?: string; docId?: string; bytes?: number[]; edgeId?: string };
|
|
67
|
+
try {
|
|
68
|
+
payload = await request.json();
|
|
69
|
+
} catch {
|
|
70
|
+
return new Response(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
success: false,
|
|
73
|
+
error: "Invalid JSON payload",
|
|
74
|
+
}),
|
|
75
|
+
{
|
|
76
|
+
status: 400,
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate payload
|
|
83
|
+
if (!payload.collection || typeof payload.collection !== "string") {
|
|
84
|
+
return new Response(
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
success: false,
|
|
87
|
+
error: "Missing or invalid collection field",
|
|
88
|
+
}),
|
|
89
|
+
{
|
|
90
|
+
status: 400,
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!payload.docId || typeof payload.docId !== "string") {
|
|
97
|
+
return new Response(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
success: false,
|
|
100
|
+
error: "Missing or invalid docId field",
|
|
101
|
+
}),
|
|
102
|
+
{
|
|
103
|
+
status: 400,
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!Array.isArray(payload.bytes)) {
|
|
110
|
+
return new Response(
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
success: false,
|
|
113
|
+
error: "Missing or invalid bytes field",
|
|
114
|
+
}),
|
|
115
|
+
{
|
|
116
|
+
status: 400,
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Convert bytes array to ArrayBuffer for Convex
|
|
123
|
+
const bytes = new Uint8Array(payload.bytes).buffer;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Call the internal mutation that skips outbox
|
|
127
|
+
// The mutation uses ctx.db directly (not wrapped with triggers)
|
|
128
|
+
// to prevent sync loops back to edge
|
|
129
|
+
const result = await ctx.runMutation(api.sync.applyDeltaFromEdge, {
|
|
130
|
+
collection: payload.collection,
|
|
131
|
+
docId: payload.docId,
|
|
132
|
+
bytes,
|
|
133
|
+
authToken: token,
|
|
134
|
+
edgeId: payload.edgeId,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return new Response(
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
success: true,
|
|
140
|
+
seq: result.seq,
|
|
141
|
+
}),
|
|
142
|
+
{
|
|
143
|
+
status: 200,
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
149
|
+
return new Response(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
success: false,
|
|
152
|
+
error: message,
|
|
153
|
+
}),
|
|
154
|
+
{
|
|
155
|
+
status: 500,
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* GET /api/dink/getSnapshot
|
|
165
|
+
*
|
|
166
|
+
* Returns the current document state as a Yjs snapshot for edge clients
|
|
167
|
+
* to bootstrap their local state or reconnect after disconnection.
|
|
168
|
+
*
|
|
169
|
+
* Query Parameters:
|
|
170
|
+
* - collection: string - The collection/table name
|
|
171
|
+
* - docId: string - The document sync ID
|
|
172
|
+
*
|
|
173
|
+
* Headers:
|
|
174
|
+
* - Authorization: Bearer <appSyncKey>
|
|
175
|
+
*
|
|
176
|
+
* Response:
|
|
177
|
+
* - 200: { success: true, bytes: number[], seq: number }
|
|
178
|
+
* - 400: { success: false, error: string } - Invalid parameters
|
|
179
|
+
* - 401: { success: false, error: string } - Unauthorized
|
|
180
|
+
* - 404: { success: false, error: string } - Document not found
|
|
181
|
+
* - 500: { success: false, error: string } - Server error
|
|
182
|
+
*/
|
|
183
|
+
http.route({
|
|
184
|
+
path: "/api/dink/getSnapshot",
|
|
185
|
+
method: "GET",
|
|
186
|
+
handler: httpAction(async (ctx, request) => {
|
|
187
|
+
// Validate Authorization header
|
|
188
|
+
const authHeader = request.headers.get("Authorization");
|
|
189
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
190
|
+
return new Response(
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
success: false,
|
|
193
|
+
error: "Missing or invalid Authorization header",
|
|
194
|
+
}),
|
|
195
|
+
{
|
|
196
|
+
status: 401,
|
|
197
|
+
headers: { "Content-Type": "application/json" },
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const token = authHeader.slice(7).trim();
|
|
203
|
+
if (!token) {
|
|
204
|
+
return new Response(
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
success: false,
|
|
207
|
+
error: "Missing authorization token",
|
|
208
|
+
}),
|
|
209
|
+
{
|
|
210
|
+
status: 401,
|
|
211
|
+
headers: { "Content-Type": "application/json" },
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Parse query parameters
|
|
217
|
+
const url = new URL(request.url);
|
|
218
|
+
const collection = url.searchParams.get("collection");
|
|
219
|
+
const docId = url.searchParams.get("docId");
|
|
220
|
+
|
|
221
|
+
// Validate query parameters
|
|
222
|
+
if (!collection || typeof collection !== "string") {
|
|
223
|
+
return new Response(
|
|
224
|
+
JSON.stringify({
|
|
225
|
+
success: false,
|
|
226
|
+
error: "Missing or invalid collection parameter",
|
|
227
|
+
}),
|
|
228
|
+
{
|
|
229
|
+
status: 400,
|
|
230
|
+
headers: { "Content-Type": "application/json" },
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!docId || typeof docId !== "string") {
|
|
236
|
+
return new Response(
|
|
237
|
+
JSON.stringify({
|
|
238
|
+
success: false,
|
|
239
|
+
error: "Missing or invalid docId parameter",
|
|
240
|
+
}),
|
|
241
|
+
{
|
|
242
|
+
status: 400,
|
|
243
|
+
headers: { "Content-Type": "application/json" },
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Query the document state
|
|
250
|
+
const result = await ctx.runQuery(api.sync.getDocumentState, {
|
|
251
|
+
collection,
|
|
252
|
+
docId,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Return 404 if document not found
|
|
256
|
+
if (!result) {
|
|
257
|
+
return new Response(
|
|
258
|
+
JSON.stringify({
|
|
259
|
+
success: false,
|
|
260
|
+
error: "Document not found",
|
|
261
|
+
}),
|
|
262
|
+
{
|
|
263
|
+
status: 404,
|
|
264
|
+
headers: { "Content-Type": "application/json" },
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Convert ArrayBuffer to number array for JSON serialization
|
|
270
|
+
const bytes = Array.from(new Uint8Array(result.bytes));
|
|
271
|
+
|
|
272
|
+
return new Response(
|
|
273
|
+
JSON.stringify({
|
|
274
|
+
success: true,
|
|
275
|
+
bytes,
|
|
276
|
+
seq: result.seq,
|
|
277
|
+
}),
|
|
278
|
+
{
|
|
279
|
+
status: 200,
|
|
280
|
+
headers: { "Content-Type": "application/json" },
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
285
|
+
return new Response(
|
|
286
|
+
JSON.stringify({
|
|
287
|
+
success: false,
|
|
288
|
+
error: message,
|
|
289
|
+
}),
|
|
290
|
+
{
|
|
291
|
+
status: 500,
|
|
292
|
+
headers: { "Content-Type": "application/json" },
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* GET /api/dink/listDocuments
|
|
301
|
+
*
|
|
302
|
+
* Returns a list of document IDs (syncId values) in a collection.
|
|
303
|
+
* Supports pagination via cursor parameter.
|
|
304
|
+
*
|
|
305
|
+
* Query Parameters:
|
|
306
|
+
* - collection: string - The collection/table name
|
|
307
|
+
* - cursor: string (optional) - Pagination cursor
|
|
308
|
+
* - limit: number (optional) - Max results per page (default: 100)
|
|
309
|
+
*
|
|
310
|
+
* Headers:
|
|
311
|
+
* - Authorization: Bearer <appSyncKey>
|
|
312
|
+
*
|
|
313
|
+
* Response:
|
|
314
|
+
* - 200: { success: true, docIds: string[], nextCursor?: string }
|
|
315
|
+
* - 400: { success: false, error: string } - Invalid parameters
|
|
316
|
+
* - 401: { success: false, error: string } - Unauthorized
|
|
317
|
+
* - 500: { success: false, error: string } - Server error
|
|
318
|
+
*/
|
|
319
|
+
http.route({
|
|
320
|
+
path: "/api/dink/listDocuments",
|
|
321
|
+
method: "GET",
|
|
322
|
+
handler: httpAction(async (ctx, request) => {
|
|
323
|
+
// Validate Authorization header
|
|
324
|
+
const authHeader = request.headers.get("Authorization");
|
|
325
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
326
|
+
return new Response(
|
|
327
|
+
JSON.stringify({
|
|
328
|
+
success: false,
|
|
329
|
+
error: "Missing or invalid Authorization header",
|
|
330
|
+
}),
|
|
331
|
+
{
|
|
332
|
+
status: 401,
|
|
333
|
+
headers: { "Content-Type": "application/json" },
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const token = authHeader.slice(7).trim();
|
|
339
|
+
if (!token) {
|
|
340
|
+
return new Response(
|
|
341
|
+
JSON.stringify({
|
|
342
|
+
success: false,
|
|
343
|
+
error: "Missing authorization token",
|
|
344
|
+
}),
|
|
345
|
+
{
|
|
346
|
+
status: 401,
|
|
347
|
+
headers: { "Content-Type": "application/json" },
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Parse query parameters
|
|
353
|
+
const url = new URL(request.url);
|
|
354
|
+
const collection = url.searchParams.get("collection");
|
|
355
|
+
const cursor = url.searchParams.get("cursor") || undefined;
|
|
356
|
+
const limitParam = url.searchParams.get("limit");
|
|
357
|
+
const limit = limitParam ? parseInt(limitParam, 10) : 100;
|
|
358
|
+
|
|
359
|
+
// Validate query parameters
|
|
360
|
+
if (!collection || typeof collection !== "string") {
|
|
361
|
+
return new Response(
|
|
362
|
+
JSON.stringify({
|
|
363
|
+
success: false,
|
|
364
|
+
error: "Missing or invalid collection parameter",
|
|
365
|
+
}),
|
|
366
|
+
{
|
|
367
|
+
status: 400,
|
|
368
|
+
headers: { "Content-Type": "application/json" },
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isNaN(limit) || limit < 1) {
|
|
374
|
+
return new Response(
|
|
375
|
+
JSON.stringify({
|
|
376
|
+
success: false,
|
|
377
|
+
error: "Invalid limit parameter",
|
|
378
|
+
}),
|
|
379
|
+
{
|
|
380
|
+
status: 400,
|
|
381
|
+
headers: { "Content-Type": "application/json" },
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// Query the document list
|
|
388
|
+
const result = await ctx.runQuery(api.sync.listDocumentsPaginated, {
|
|
389
|
+
collection,
|
|
390
|
+
cursor,
|
|
391
|
+
limit,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return new Response(
|
|
395
|
+
JSON.stringify({
|
|
396
|
+
success: true,
|
|
397
|
+
docIds: result.docIds,
|
|
398
|
+
nextCursor: result.nextCursor,
|
|
399
|
+
}),
|
|
400
|
+
{
|
|
401
|
+
status: 200,
|
|
402
|
+
headers: { "Content-Type": "application/json" },
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
407
|
+
return new Response(
|
|
408
|
+
JSON.stringify({
|
|
409
|
+
success: false,
|
|
410
|
+
error: message,
|
|
411
|
+
}),
|
|
412
|
+
{
|
|
413
|
+
status: 500,
|
|
414
|
+
headers: { "Content-Type": "application/json" },
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
export default http;
|
package/convex/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fatagnus/dink-convex - Internal Convex Functions
|
|
3
|
+
*
|
|
4
|
+
* This directory contains the component's internal Convex code including:
|
|
5
|
+
* - Internal sync tables schema
|
|
6
|
+
* - HTTP endpoints for dinkd communication
|
|
7
|
+
* - Scheduled functions for outbox processing
|
|
8
|
+
* - Trigger setup for intercepting writes
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Placeholder exports - will be implemented in subsequent stories
|
|
14
|
+
// US-003: Schema
|
|
15
|
+
// US-004: Triggers
|
|
16
|
+
// US-006-008: HTTP endpoints
|
|
17
|
+
// US-009: Scheduled functions
|
|
18
|
+
|
|
19
|
+
export const COMPONENT_NAME = "@fatagnus/dink-convex";
|
|
20
|
+
export const COMPONENT_VERSION = "0.1.0";
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installation and configuration mutations for @fatagnus/dink-convex component.
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to enable/disable sync for specific collections
|
|
5
|
+
* by managing the _sync_config table.
|
|
6
|
+
*
|
|
7
|
+
* @module convex/install
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { v } from "convex/values";
|
|
11
|
+
import { mutation, query } from "./_generated/server";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Enable sync for a collection.
|
|
15
|
+
*
|
|
16
|
+
* Registers the collection in _sync_config with enabled=true.
|
|
17
|
+
* If the collection is already registered, updates it to enabled.
|
|
18
|
+
*
|
|
19
|
+
* @param collection - The name of the collection/table to enable sync for
|
|
20
|
+
* @returns Object with success status
|
|
21
|
+
*/
|
|
22
|
+
export const enableSync = mutation({
|
|
23
|
+
args: {
|
|
24
|
+
collection: v.string(),
|
|
25
|
+
},
|
|
26
|
+
returns: v.object({
|
|
27
|
+
success: v.boolean(),
|
|
28
|
+
message: v.string(),
|
|
29
|
+
}),
|
|
30
|
+
handler: async (ctx, args) => {
|
|
31
|
+
// Check if collection already exists in _sync_config
|
|
32
|
+
const existing = await ctx.db
|
|
33
|
+
.query("sync_config")
|
|
34
|
+
.withIndex("by_collection", (q) => q.eq("collection", args.collection))
|
|
35
|
+
.first();
|
|
36
|
+
|
|
37
|
+
if (existing) {
|
|
38
|
+
// Update existing config to enabled
|
|
39
|
+
if (!existing.enabled) {
|
|
40
|
+
await ctx.db.patch(existing._id, { enabled: true });
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
message: `Sync enabled for collection: ${args.collection}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
message: `Sync already enabled for collection: ${args.collection}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create new config entry
|
|
53
|
+
await ctx.db.insert("sync_config", {
|
|
54
|
+
collection: args.collection,
|
|
55
|
+
enabled: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
message: `Sync enabled for collection: ${args.collection}`,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Disable sync for a collection.
|
|
67
|
+
*
|
|
68
|
+
* Updates the collection's _sync_config entry to enabled=false.
|
|
69
|
+
* Does not delete the config entry to preserve history.
|
|
70
|
+
*
|
|
71
|
+
* @param collection - The name of the collection/table to disable sync for
|
|
72
|
+
* @returns Object with success status
|
|
73
|
+
*/
|
|
74
|
+
export const disableSync = mutation({
|
|
75
|
+
args: {
|
|
76
|
+
collection: v.string(),
|
|
77
|
+
},
|
|
78
|
+
returns: v.object({
|
|
79
|
+
success: v.boolean(),
|
|
80
|
+
message: v.string(),
|
|
81
|
+
}),
|
|
82
|
+
handler: async (ctx, args) => {
|
|
83
|
+
// Check if collection exists in _sync_config
|
|
84
|
+
const existing = await ctx.db
|
|
85
|
+
.query("sync_config")
|
|
86
|
+
.withIndex("by_collection", (q) => q.eq("collection", args.collection))
|
|
87
|
+
.first();
|
|
88
|
+
|
|
89
|
+
if (!existing) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
message: `Collection not found in sync config: ${args.collection}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!existing.enabled) {
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
message: `Sync already disabled for collection: ${args.collection}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Update to disabled
|
|
104
|
+
await ctx.db.patch(existing._id, { enabled: false });
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
message: `Sync disabled for collection: ${args.collection}`,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get sync status for a collection.
|
|
115
|
+
*
|
|
116
|
+
* Queries _sync_config to determine if sync is enabled for the collection.
|
|
117
|
+
*
|
|
118
|
+
* @param collection - The name of the collection/table to check
|
|
119
|
+
* @returns Object with enabled status, or null if not configured
|
|
120
|
+
*/
|
|
121
|
+
export const getSyncStatus = query({
|
|
122
|
+
args: {
|
|
123
|
+
collection: v.string(),
|
|
124
|
+
},
|
|
125
|
+
returns: v.union(
|
|
126
|
+
v.object({
|
|
127
|
+
collection: v.string(),
|
|
128
|
+
enabled: v.boolean(),
|
|
129
|
+
}),
|
|
130
|
+
v.null()
|
|
131
|
+
),
|
|
132
|
+
handler: async (ctx, args) => {
|
|
133
|
+
const config = await ctx.db
|
|
134
|
+
.query("sync_config")
|
|
135
|
+
.withIndex("by_collection", (q) => q.eq("collection", args.collection))
|
|
136
|
+
.first();
|
|
137
|
+
|
|
138
|
+
if (!config) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
collection: config.collection,
|
|
144
|
+
enabled: config.enabled,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List all collections with sync configuration.
|
|
151
|
+
*
|
|
152
|
+
* Returns all collections in _sync_config with their enabled status.
|
|
153
|
+
*
|
|
154
|
+
* @returns Array of collection configs
|
|
155
|
+
*/
|
|
156
|
+
export const listSyncedCollections = query({
|
|
157
|
+
args: {},
|
|
158
|
+
returns: v.array(
|
|
159
|
+
v.object({
|
|
160
|
+
collection: v.string(),
|
|
161
|
+
enabled: v.boolean(),
|
|
162
|
+
})
|
|
163
|
+
),
|
|
164
|
+
handler: async (ctx) => {
|
|
165
|
+
const configs = await ctx.db.query("sync_config").collect();
|
|
166
|
+
|
|
167
|
+
return configs.map((config) => ({
|
|
168
|
+
collection: config.collection,
|
|
169
|
+
enabled: config.enabled,
|
|
170
|
+
}));
|
|
171
|
+
},
|
|
172
|
+
});
|