cogsbox-sync 0.0.1

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.
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "my-sync-worker",
3
+ "version": "0.0.1",
4
+ "scripts": {
5
+ "init": "tsx bin/init.ts",
6
+ "init:deploy": "tsx bin/init.ts --deploy",
7
+ "deploy": "wrangler deploy",
8
+ "dev": "wrangler dev --port 8788",
9
+ "start": "wrangler dev",
10
+ "test": "vitest",
11
+ "cf-typegen": "wrangler types"
12
+ },
13
+ "devDependencies": {
14
+ "@cloudflare/vitest-pool-workers": "^0.12.12",
15
+ "@cloudflare/workers-types": "^4.20260214.0",
16
+ "@vitest/runner": "3.2.4",
17
+ "@vitest/snapshot": "3.2.4",
18
+ "tsx": "^4.21.0",
19
+ "typescript": "^5.9.3",
20
+ "vitest": "~3.2.4",
21
+ "wrangler": "^4.65.0"
22
+ },
23
+ "dependencies": {
24
+ "@cfworker/json-schema": "^4.1.1",
25
+ "@cloudflare/containers": "^0.1.0",
26
+ "@tsndr/cloudflare-worker-jwt": "^3.2.1",
27
+ "cogsbox-shape": "^0.5.158",
28
+ "fast-json-patch": "^3.1.1"
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ # schema-processor/Dockerfile
2
+ FROM node:20-alpine
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package.json first
7
+ COPY package.json ./
8
+ RUN npm install
9
+
10
+ # Copy server code
11
+ COPY server.js ./
12
+
13
+ # Expose the port
14
+ EXPOSE 8080
15
+
16
+ CMD ["node", "server.js"]
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "schema-processor",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "hono":"^4.10.4"
6
+ },
7
+ "devDependencies": {
8
+ "@hono/node-server": "^1.19.6"
9
+ }
10
+ }
@@ -0,0 +1,483 @@
1
+ // schema-processor/server.js - Hono version
2
+ import { Hono } from 'hono';
3
+ import { cors } from 'hono/cors';
4
+ import fs from 'fs';
5
+
6
+ const app = new Hono();
7
+
8
+ // Enable CORS if needed
9
+ app.use('*', cors());
10
+
11
+ // In-memory cache for schemas per tenant
12
+ const schemaCache = new Map();
13
+
14
+ // Utility function to safely import schema module
15
+ async function importSchemaModule(filePath) {
16
+ try {
17
+
18
+
19
+ const schemaModule = await import(`file://${filePath}?t=${Date.now()}`);
20
+
21
+ if (!schemaModule.syncSchema) {
22
+ throw new Error('syncSchema not found in module');
23
+ }
24
+
25
+ return schemaModule;
26
+ } catch (error) {
27
+ console.error(`Failed to import schema module from ${filePath}:`, error.message);
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ // Load schema for a tenant and cache it
33
+ async function loadSchemaForTenant(tenantId) {
34
+ console.log(`[loadSchemaForTenant] Starting load for tenant ${tenantId}`);
35
+
36
+ if (schemaCache.has(tenantId)) {
37
+ console.log(`[loadSchemaForTenant] Schema already cached for tenant ${tenantId}`);
38
+ return schemaCache.get(tenantId);
39
+ }
40
+
41
+ // Load from file system cache first
42
+ const cacheFile = `/tmp/schema-${tenantId}.mjs`;
43
+ console.log(`[loadSchemaForTenant] Checking cache file: ${cacheFile}`);
44
+
45
+ if (fs.existsSync(cacheFile)) {
46
+ try {
47
+ console.log(`[loadSchemaForTenant] Cache file exists, importing...`);
48
+ const schemaModule = await importSchemaModule(cacheFile);
49
+
50
+ schemaCache.set(tenantId, schemaModule.syncSchema);
51
+ console.log(`[loadSchemaForTenant] Schema cached successfully for tenant ${tenantId}`);
52
+ return schemaModule.syncSchema;
53
+ } catch (error) {
54
+ console.error(`[loadSchemaForTenant] Cache file import failed:`, error.message);
55
+ console.log('Cache file invalid, will reload:', error.message);
56
+ try {
57
+ fs.unlinkSync(cacheFile);
58
+ console.log(`[loadSchemaForTenant] Removed invalid cache file`);
59
+ } catch (unlinkError) {
60
+ console.error(`[loadSchemaForTenant] Failed to remove cache file:`, unlinkError.message);
61
+ }
62
+ }
63
+ } else {
64
+ console.log(`[loadSchemaForTenant] Cache file does not exist`);
65
+ }
66
+
67
+ console.log(`[loadSchemaForTenant] No schema available for tenant ${tenantId}`);
68
+ return null;
69
+ }
70
+
71
+ // Health check endpoint
72
+ app.get('/health', (c) => {
73
+ return c.json({
74
+ status: 'healthy',
75
+ timestamp: new Date().toISOString(),
76
+ cachedSchemas: Array.from(schemaCache.keys())
77
+ });
78
+ });
79
+
80
+ // GET endpoint to check schema status
81
+ app.get('/load-schema', async (c) => {
82
+ try {
83
+ const tenantId = c.req.query('tenantId');
84
+
85
+ if (!tenantId) {
86
+ return c.json({ error: 'tenantId is required' }, 400);
87
+ }
88
+
89
+ const schema = await loadSchemaForTenant(tenantId);
90
+
91
+ // Handle both schema formats - direct schemas or nested under 'schemas'
92
+ let schemaKeys = [];
93
+ if (schema) {
94
+ if (schema.schemas) {
95
+ schemaKeys = Object.keys(schema.schemas);
96
+ } else {
97
+ schemaKeys = Object.keys(schema);
98
+ }
99
+ }
100
+
101
+ return c.json({
102
+ hasSchema: !!schema,
103
+ schemaKeys: schemaKeys
104
+ });
105
+ } catch (error) {
106
+ console.error('Error in GET /load-schema:', error);
107
+ return c.json({ error: error.message }, 500);
108
+ }
109
+ });
110
+
111
+ // POST endpoint to load/update schema
112
+ app.post('/load-schema', async (c) => {
113
+ try {
114
+ const tenantId = c.req.query('tenantId');
115
+ const { bundledCode } = await c.req.json();
116
+
117
+ if (!tenantId) {
118
+ return c.json({ error: 'tenantId is required' }, 400);
119
+ }
120
+
121
+ if (bundledCode) {
122
+ // Save and cache new schema
123
+ const cacheFile = `/tmp/schema-${tenantId}.mjs`;
124
+ fs.writeFileSync(cacheFile, bundledCode);
125
+
126
+ const schemaModule = await importSchemaModule(cacheFile);
127
+ schemaCache.set(tenantId, schemaModule.syncSchema);
128
+
129
+ console.log(`Schema loaded and cached for tenant ${tenantId}`);
130
+ }
131
+
132
+ const schema = await loadSchemaForTenant(tenantId);
133
+
134
+ // Handle both schema formats
135
+ let schemaKeys = [];
136
+ if (schema) {
137
+ if (schema.schemas) {
138
+ schemaKeys = Object.keys(schema.schemas);
139
+ } else {
140
+ schemaKeys = Object.keys(schema);
141
+ }
142
+ }
143
+
144
+ return c.json({
145
+ success: true,
146
+ hasSchema: !!schema,
147
+ schemaKeys: schemaKeys
148
+ });
149
+ } catch (error) {
150
+ console.error('Error loading schema:', error);
151
+ return c.json({ error: error.message }, 500);
152
+ }
153
+ });
154
+
155
+ // Validate endpoint
156
+ app.post('/validate', async (c) => {
157
+ try {
158
+ const { data, schemaKey, bundledCode, context } = await c.req.json();
159
+ const tenantId = c.req.query('tenantId');
160
+
161
+ if (!tenantId || !schemaKey || data === undefined || !bundledCode) {
162
+ return c.json({
163
+ valid: false,
164
+ errors: [{ path: [], message: 'Missing tenantId, schemaKey, data, or bundledCode' }]
165
+ }, 400);
166
+ }
167
+
168
+ const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
169
+ fs.writeFileSync(tempFile, bundledCode);
170
+
171
+ const schemaModule = await importSchemaModule(tempFile);
172
+ fs.unlinkSync(tempFile);
173
+
174
+ let schemaDefinition = null;
175
+ if (schemaModule.syncSchema && schemaModule.syncSchema.schemas && schemaModule.syncSchema.schemas[schemaKey]) {
176
+ schemaDefinition = schemaModule.syncSchema.schemas[schemaKey];
177
+ }
178
+
179
+ if (!schemaDefinition) {
180
+ return c.json({
181
+ valid: false,
182
+ errors: [{ path: [], message: `Schema '${schemaKey}' not found within the provided bundle.` }]
183
+ }, 404);
184
+ }
185
+
186
+ const validator = schemaDefinition.validate;
187
+ if (typeof validator !== 'function') {
188
+ throw new Error(`Validator function not found for schema '${schemaKey}'`);
189
+ }
190
+
191
+ // Pass the context from the request
192
+ const result = validator(data, context || {});
193
+
194
+ if (!result.success) {
195
+ return c.json({
196
+ valid: false,
197
+ errors: result.error.issues // Using the correct "issues" property from Zod
198
+ }, 422);
199
+ }
200
+
201
+ return c.json({ valid: true, errors: [] });
202
+
203
+ } catch (error) {
204
+ console.error('[Container] /validate Error:', error);
205
+ if (error.name === 'ZodError') {
206
+ return c.json({
207
+ valid: false,
208
+ errors: error.issues
209
+ }, 422);
210
+ }
211
+ return c.json({
212
+ valid: false,
213
+ errors: [{ path: [], message: error.message }]
214
+ }, 500);
215
+ }
216
+ });
217
+
218
+ // Validate path endpoint
219
+ app.post('/validate-path', async (c) => {
220
+ try {
221
+ const { path, value, schemaKey, bundledCode } = await c.req.json();
222
+ const tenantId = c.req.query('tenantId');
223
+
224
+ if (!tenantId || !schemaKey || !bundledCode || !path) {
225
+ return c.json({
226
+ valid: false,
227
+ errors: [{ path: [], message: 'Missing required parameters' }]
228
+ }, 400);
229
+ }
230
+
231
+ // Write and load schema
232
+ const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
233
+ fs.writeFileSync(tempFile, bundledCode);
234
+ const schemaModule = await importSchemaModule(tempFile);
235
+ fs.unlinkSync(tempFile);
236
+
237
+ // Get the specific schema from the schemas collection
238
+ const schemaDefinition = schemaModule.syncSchema?.schemas?.[schemaKey];
239
+
240
+ if (!schemaDefinition) {
241
+ return c.json({
242
+ valid: false,
243
+ errors: [{ path: [], message: `Schema '${schemaKey}' not found` }]
244
+ }, 404);
245
+ }
246
+
247
+ if (!schemaDefinition.schemas || !schemaDefinition.schemas.validation) {
248
+ return c.json({
249
+ valid: false,
250
+ errors: [{ path: [], message: `Validation schema not found for '${schemaKey}'` }]
251
+ }, 404);
252
+ }
253
+
254
+ // Walk to the target field
255
+ let fieldValidator = schemaDefinition.schemas.validation;
256
+
257
+ for (let i = 0; i < path.length; i++) {
258
+ const segment = path[i];
259
+
260
+ if (typeof segment === 'number' || segment.startsWith('id:')) {
261
+ // Array element
262
+ if (fieldValidator._def?.typeName === 'ZodArray') {
263
+ fieldValidator = fieldValidator._def.type || fieldValidator.element;
264
+ } else {
265
+ return c.json({ valid: true });
266
+ }
267
+ } else {
268
+ // Object property
269
+ if (fieldValidator.shape) {
270
+ fieldValidator = fieldValidator.shape[segment];
271
+ } else if (fieldValidator._def?.shape) {
272
+ const shape = fieldValidator._def.shape();
273
+ fieldValidator = shape[segment];
274
+ } else {
275
+ return c.json({ valid: true });
276
+ }
277
+ }
278
+
279
+ if (!fieldValidator) {
280
+ return c.json({ valid: true });
281
+ }
282
+ }
283
+
284
+ // Validate the value
285
+ const result = fieldValidator.safeParse(value);
286
+
287
+ if (!result.success) {
288
+ return c.json({
289
+ valid: false,
290
+ errors: result.error.issues.map(issue => ({
291
+ path: issue.path,
292
+ message: issue.message,
293
+ code: issue.code
294
+ }))
295
+ }, 422);
296
+ }
297
+
298
+ return c.json({ valid: true, errors: [] });
299
+
300
+ } catch (error) {
301
+ console.error('[Container] /validate-path Error:', error);
302
+ return c.json({
303
+ valid: false,
304
+ errors: [{ path: [], message: error.message }]
305
+ }, 500);
306
+ }
307
+ });
308
+
309
+ // Get API routes endpoint
310
+ app.post('/get-api-routes', async (c) => {
311
+ try {
312
+ const { tenantId, schemaKey, bundledCode, params } = await c.req.json();
313
+
314
+ if (!tenantId || !schemaKey || !bundledCode) {
315
+ return c.json({ error: 'Missing tenantId, schemaKey, or bundledCode' }, 400);
316
+ }
317
+
318
+ const justSchemaKey = schemaKey.split('[')[0];
319
+ const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
320
+ fs.writeFileSync(tempFile, bundledCode);
321
+
322
+ const schemaModule = await importSchemaModule(tempFile);
323
+ fs.unlinkSync(tempFile);
324
+
325
+ const schemaDefinition = schemaModule.syncSchema?.schemas?.[justSchemaKey];
326
+
327
+ if (!schemaDefinition) {
328
+ return c.json({ error: `Schema '${justSchemaKey}' not found in bundle.` }, 404);
329
+ }
330
+
331
+ const apiConfig = schemaDefinition.api || {};
332
+
333
+ // Process both queryData and mutateData with the same logic
334
+ const processApiConfig = (config) => {
335
+ if (!config) return null;
336
+
337
+ if (config && typeof config.handler === 'function') {
338
+ if (!params || typeof params !== 'object') {
339
+ throw new Error('Missing or invalid params object for parameterized route.');
340
+ }
341
+
342
+ const validationResult = schemaDefinition.validateApiParams(config, params);
343
+
344
+ if (!validationResult.success) {
345
+ throw new Error(`API parameter validation failed: ${JSON.stringify(validationResult.error.flatten())}`);
346
+ }
347
+
348
+ const result = config.handler(validationResult.data);
349
+
350
+ if (typeof result === 'string') {
351
+ return { url: result, method: 'GET' };
352
+ } else if (result && typeof result === 'object' && result.url) {
353
+ return {
354
+ url: result.url,
355
+ method: result.data ? 'POST' : 'GET',
356
+ data: result.data
357
+ };
358
+ }
359
+
360
+ throw new Error('Invalid handler return value');
361
+ } else if (typeof config === 'string') {
362
+ return { url: config, method: 'GET' };
363
+ }
364
+
365
+ return null;
366
+ };
367
+
368
+ try {
369
+ const queryConfig = processApiConfig(apiConfig.queryData);
370
+ const mutateConfig = processApiConfig(apiConfig.mutateData);
371
+
372
+ return c.json({
373
+ queryData: queryConfig,
374
+ mutateData: mutateConfig
375
+ });
376
+ } catch (error) {
377
+ return c.json({ error: error.message }, 400);
378
+ }
379
+
380
+ } catch (error) {
381
+ console.error('[Container] /get-api-routes Error:', error);
382
+ return c.json({ error: error.message }, 500);
383
+ }
384
+ });
385
+
386
+ // Process notifications batch endpoint
387
+ app.post('/process-notifications-batch', async (c) => {
388
+ try {
389
+ const { bundledCode, fullState, contexts, operation } = await c.req.json();
390
+ const tenantId = c.req.query('tenantId');
391
+
392
+ if (!tenantId || !bundledCode || !fullState || !Array.isArray(contexts)) {
393
+ return c.json({ error: 'Missing tenantId, bundledCode, fullState, or contexts array' }, 400);
394
+ }
395
+
396
+ const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
397
+ fs.writeFileSync(tempFile, bundledCode);
398
+ const schemaModule = await importSchemaModule(tempFile);
399
+ fs.unlinkSync(tempFile);
400
+
401
+ const schema = schemaModule.syncSchema;
402
+ if (!schema) {
403
+ return c.json({ error: 'syncSchema not found in the provided bundle.' }, 404);
404
+ }
405
+
406
+ const notificationFunctions = schema.notifications;
407
+ if (!notificationFunctions) {
408
+ return c.json({});
409
+ }
410
+
411
+ const notificationsByUserId = {};
412
+
413
+ for (const context of contexts) {
414
+ if (!context || !context.userId) continue;
415
+
416
+ const matchedNotificationsForUser = [];
417
+
418
+ for (const channelName in notificationFunctions) {
419
+ try {
420
+ const notificationFn = notificationFunctions[channelName];
421
+ const result = notificationFn(context, fullState);
422
+
423
+ if (result !== null && result !== undefined) {
424
+ matchedNotificationsForUser.push({
425
+ channel: channelName,
426
+ payload: { ...result },
427
+ });
428
+ }
429
+ } catch (fnError) {
430
+ console.error(`[Container] Error executing notification function '${channelName}' for user '${context.userId}':`, fnError);
431
+ }
432
+ }
433
+
434
+ if (matchedNotificationsForUser.length > 0) {
435
+ notificationsByUserId[context.userId] = matchedNotificationsForUser;
436
+ }
437
+ }
438
+
439
+ return c.json(notificationsByUserId);
440
+
441
+ } catch (error) {
442
+ console.error('[Container] /process-notifications-batch Error:', error);
443
+ return c.json({
444
+ error: 'Internal server error processing notification batch',
445
+ details: error.message
446
+ }, 500);
447
+ }
448
+ });
449
+
450
+ // Error handling
451
+ app.onError((err, c) => {
452
+ console.error('Unhandled error:', err);
453
+ return c.json({ error: 'Internal server error' }, 500);
454
+ });
455
+
456
+ // Graceful shutdown handlers
457
+ process.on('SIGTERM', () => {
458
+ console.log('Received SIGTERM, shutting down gracefully');
459
+ process.exit(0);
460
+ });
461
+
462
+ process.on('SIGINT', () => {
463
+ console.log('Received SIGINT, shutting down gracefully');
464
+ process.exit(0);
465
+ });
466
+
467
+ // Export for Cloudflare Container or start server for Node.js
468
+ const PORT = process.env.PORT || 8080;
469
+
470
+ // For Cloudflare Container Runtime
471
+ export default {
472
+ fetch: app.fetch,
473
+ };
474
+
475
+ // For Node.js runtime (if needed for local testing)
476
+ if (typeof process !== 'undefined' && process.versions && process.versions.node) {
477
+ const { serve } = await import('@hono/node-server');
478
+ serve({
479
+ fetch: app.fetch,
480
+ port: PORT,
481
+ });
482
+ console.log(`Schema processor listening on port ${PORT}`);
483
+ }